Remove static binding to ffmpeg and use command instead

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 2 years ago
parent bdc3ae07ac
commit 9ad9ccf778
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -4,22 +4,16 @@ readme = "README.md"
license = "Apache-2.0"
authors = ["trivernis <trivernis@protonmail.com>"]
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"]
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"

@ -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<image::error::ImageError> for ThumbError {
Self::Image(e)
}
}
#[cfg(feature = "ffmpeg")]
impl From<ffmpeg_next::Error> for ThumbError {
fn from(e: ffmpeg_next::Error) -> Self {
Self::FFMPEG(e)
}
}

@ -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<R: BufRead + Seek>(reader: R, mime: Mime) -> ThumbResult<DynamicImage> {
match mime.type_() {
mime::IMAGE => read_image(reader, mime),
#[cfg(feature = "ffmpeg")]
mime::VIDEO => get_video_frame(reader, mime),
_ => Err(ThumbError::Unsupported(mime)),
}

@ -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<R: BufRead + Seek>(mut reader: R, mime: Mime) -> ThumbResult<DynamicImage> {
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<R: BufRead + Seek>(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<DynamicImage> {
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<Graph> {
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<DynamicImage> {
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)
}

@ -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 {

@ -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<Vec<u8>> {
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<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(args: I) -> ThumbResult<Vec<u8>> {
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
}
}
}
}

@ -0,0 +1 @@
pub mod ffmpeg_cli;

@ -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);
}
},
}
}

Loading…
Cancel
Save