Add ffprobe for details inspection - vastly improve video detection speed

This commit is contained in:
asonix 2022-09-25 20:39:09 -05:00
parent 5449bb82f1
commit 80c83eb491
7 changed files with 206 additions and 71 deletions

View file

@ -110,8 +110,8 @@ pub(crate) enum Store {
} }
impl ImageFormat { impl ImageFormat {
pub(crate) fn as_hint(self) -> Option<ValidInputType> { pub(crate) fn as_hint(self) -> ValidInputType {
Some(ValidInputType::from_format(self)) ValidInputType::from_format(self)
} }
pub(crate) fn as_magick_format(self) -> &'static str { pub(crate) fn as_magick_format(self) -> &'static str {

View file

@ -24,11 +24,18 @@ impl Details {
} }
#[tracing::instrument("Details from bytes", skip(input))] #[tracing::instrument("Details from bytes", skip(input))]
pub(crate) async fn from_bytes( pub(crate) async fn from_bytes(input: web::Bytes, hint: ValidInputType) -> Result<Self, Error> {
input: web::Bytes, let details = if hint.is_video() {
hint: Option<ValidInputType>, crate::ffmpeg::details_bytes(input.clone()).await?
) -> Result<Self, Error> { } else {
let details = crate::magick::details_bytes(input, hint).await?; None
};
let details = if let Some(details) = details {
details
} else {
crate::magick::details_bytes(input, Some(hint)).await?
};
Ok(Details::now( Ok(Details::now(
details.width, details.width,
@ -44,7 +51,17 @@ impl Details {
identifier: S::Identifier, identifier: S::Identifier,
expected_format: Option<ValidInputType>, expected_format: Option<ValidInputType>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let details = crate::magick::details_store(store, identifier, expected_format).await?; let details = if expected_format.map(|t| t.is_video()).unwrap_or(true) {
crate::ffmpeg::details_store(&store, &identifier).await?
} else {
None
};
let details = if let Some(details) = details {
details
} else {
crate::magick::details_store(store, identifier, expected_format).await?
};
Ok(Details::now( Ok(Details::now(
details.width, details.width,
@ -54,7 +71,12 @@ impl Details {
)) ))
} }
pub(crate) fn now(width: usize, height: usize, content_type: mime::Mime, frames: Option<usize>) -> Self { pub(crate) fn now(
width: usize,
height: usize,
content_type: mime::Mime,
frames: Option<usize>,
) -> Self {
Details { Details {
width, width,
height, height,

View file

@ -1,8 +1,8 @@
use crate::{ use crate::{
error::{Error, UploadError}, error::{Error, UploadError},
magick::{Details, ValidInputType},
process::Process, process::Process,
store::Store, store::Store,
magick::ValidInputType,
}; };
use actix_web::web::Bytes; use actix_web::web::Bytes;
use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::io::{AsyncRead, AsyncReadExt};
@ -30,11 +30,11 @@ impl InputFormat {
} }
} }
pub(crate) fn to_valid_input_type(self) -> ValidInputType { fn to_mime(self) -> mime::Mime {
match self { match self {
Self::Gif => ValidInputType::Gif, Self::Gif => mime::IMAGE_GIF,
Self::Mp4 => ValidInputType::Mp4, Self::Mp4 => crate::magick::video_mp4(),
Self::Webm => ValidInputType::Webm, Self::Webm => crate::magick::video_webm(),
} }
} }
} }
@ -67,9 +67,53 @@ const FORMAT_MAPPINGS: &[(&str, InputFormat)] = &[
("webm", InputFormat::Webm), ("webm", InputFormat::Webm),
]; ];
pub(crate) async fn input_type_bytes( pub(crate) async fn input_type_bytes(input: Bytes) -> Result<Option<ValidInputType>, Error> {
input: Bytes, if let Some(details) = details_bytes(input).await? {
) -> Result<Option<InputFormat>, Error> { return Ok(Some(details.validate_input()?));
}
Ok(None)
}
pub(crate) async fn details_store<S: Store>(
store: &S,
identifier: &S::Identifier,
) -> Result<Option<Details>, Error> {
let input_file = crate::tmp_file::tmp_file(None);
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
crate::store::file_store::safe_create_parent(&input_file).await?;
let mut tmp_one = crate::file::File::create(&input_file).await?;
tmp_one
.write_from_stream(store.to_stream(&identifier, None, None).await?)
.await?;
tmp_one.close().await?;
let process = Process::run(
"ffprobe",
&[
"-v",
"quiet",
"-select_streams",
"v:0",
"-count_frames",
"-show_entries",
"stream=width,height,nb_read_frames:format=format_name",
"-of",
"default=noprint_wrappers=1:nokey=1",
input_file_str,
],
)?;
let mut output = Vec::new();
process.read().read_to_end(&mut output).await?;
let output = String::from_utf8_lossy(&output);
tokio::fs::remove_file(input_file_str).await?;
parse_details(output)
}
pub(crate) async fn details_bytes(input: Bytes) -> Result<Option<Details>, Error> {
let input_file = crate::tmp_file::tmp_file(None); let input_file = crate::tmp_file::tmp_file(None);
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?; let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
crate::store::file_store::safe_create_parent(&input_file).await?; crate::store::file_store::safe_create_parent(&input_file).await?;
@ -78,31 +122,82 @@ pub(crate) async fn input_type_bytes(
tmp_one.write_from_bytes(input).await?; tmp_one.write_from_bytes(input).await?;
tmp_one.close().await?; tmp_one.close().await?;
let process = Process::run("ffprobe", &[ let process = Process::run(
"ffprobe",
&[
"-v", "-v",
"quiet", "quiet",
"-select_streams",
"v:0",
"-count_frames",
"-show_entries", "-show_entries",
"format=format_name", "stream=width,height,nb_read_frames:format=format_name",
"-of", "-of",
"default=noprint_wrappers=1:nokey=1", "default=noprint_wrappers=1:nokey=1",
input_file_str, input_file_str,
])?; ],
)?;
let mut output = Vec::new(); let mut output = Vec::new();
process.read().read_to_end(&mut output).await?; process.read().read_to_end(&mut output).await?;
let formats = String::from_utf8_lossy(&output); let output = String::from_utf8_lossy(&output);
tokio::fs::remove_file(input_file_str).await?;
tracing::info!("FORMATS: {}", formats); parse_details(output)
}
fn parse_details(output: std::borrow::Cow<'_, str>) -> Result<Option<Details>, Error> {
tracing::info!("OUTPUT: {}", output);
let mut lines = output.lines();
let width = match lines.next() {
Some(line) => line,
None => return Ok(None),
};
let height = match lines.next() {
Some(line) => line,
None => return Ok(None),
};
let frames = match lines.next() {
Some(line) => line,
None => return Ok(None),
};
let formats = match lines.next() {
Some(line) => line,
None => return Ok(None),
};
for (k, v) in FORMAT_MAPPINGS { for (k, v) in FORMAT_MAPPINGS {
if formats.contains(k) { if formats.contains(k) {
return Ok(Some(*v)) return Ok(Some(parse_details_inner(width, height, frames, *v)?));
} }
} }
Ok(None) Ok(None)
} }
fn parse_details_inner(
width: &str,
height: &str,
frames: &str,
format: InputFormat,
) -> Result<Details, Error> {
let width = width.parse().map_err(|_| UploadError::UnsupportedFormat)?;
let height = height.parse().map_err(|_| UploadError::UnsupportedFormat)?;
let frames = frames.parse().map_err(|_| UploadError::UnsupportedFormat)?;
Ok(Details {
mime_type: format.to_mime(),
width,
height,
frames: Some(frames),
})
}
#[tracing::instrument(name = "Convert to Mp4", skip(input))] #[tracing::instrument(name = "Convert to Mp4", skip(input))]
pub(crate) async fn to_mp4_bytes( pub(crate) async fn to_mp4_bytes(
input: Bytes, input: Bytes,
@ -122,36 +217,42 @@ pub(crate) async fn to_mp4_bytes(
tmp_one.close().await?; tmp_one.close().await?;
let process = if permit_audio { let process = if permit_audio {
Process::run("ffmpeg", &[ Process::run(
"-i", "ffmpeg",
input_file_str, &[
"-pix_fmt", "-i",
"yuv420p", input_file_str,
"-vf", "-pix_fmt",
"scale=trunc(iw/2)*2:trunc(ih/2)*2", "yuv420p",
"-c:a", "-vf",
"aac", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
"-c:v", "-c:a",
"h264", "aac",
"-f", "-c:v",
"mp4", "h264",
output_file_str, "-f",
])? "mp4",
output_file_str,
],
)?
} else { } else {
Process::run("ffmpeg", &[ Process::run(
"-i", "ffmpeg",
input_file_str, &[
"-pix_fmt", "-i",
"yuv420p", input_file_str,
"-vf", "-pix_fmt",
"scale=trunc(iw/2)*2:trunc(ih/2)*2", "yuv420p",
"-an", "-vf",
"-c:v", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
"h264", "-an",
"-f", "-c:v",
"mp4", "h264",
output_file_str, "-f",
])? "mp4",
output_file_str,
],
)?
}; };
process.wait().await?; process.wait().await?;

View file

@ -27,11 +27,11 @@ fn image_webp() -> mime::Mime {
"image/webp".parse().unwrap() "image/webp".parse().unwrap()
} }
fn video_mp4() -> mime::Mime { pub(crate) fn video_mp4() -> mime::Mime {
"video/mp4".parse().unwrap() "video/mp4".parse().unwrap()
} }
fn video_webm() -> mime::Mime { pub(crate) fn video_webm() -> mime::Mime {
"video/webm".parse().unwrap() "video/webm".parse().unwrap()
} }
@ -68,6 +68,13 @@ impl ValidInputType {
} }
} }
pub(crate) fn is_video(self) -> bool {
match self {
Self::Mp4 | Self::Webm | Self::Gif => true,
_ => false,
}
}
fn video_hint(self) -> Option<&'static str> { fn video_hint(self) -> Option<&'static str> {
match self { match self {
Self::Mp4 => Some(".mp4"), Self::Mp4 => Some(".mp4"),
@ -275,11 +282,7 @@ fn parse_details(s: std::borrow::Cow<'_, str>) -> Result<Details, Error> {
mime_type, mime_type,
width, width,
height, height,
frames: if frames > 1 { frames: if frames > 1 { Some(frames) } else { None },
Some(frames)
} else {
None
},
}) })
} }
@ -322,7 +325,7 @@ pub(crate) fn process_image_async_read<A: AsyncRead + Unpin + 'static>(
impl Details { impl Details {
#[instrument(name = "Validating input type")] #[instrument(name = "Validating input type")]
fn validate_input(&self) -> Result<ValidInputType, Error> { pub(crate) fn validate_input(&self) -> Result<ValidInputType, Error> {
if self.width > crate::CONFIG.media.max_width if self.width > crate::CONFIG.media.max_width
|| self.height > crate::CONFIG.media.max_height || self.height > crate::CONFIG.media.max_height
|| self.width * self.height > crate::CONFIG.media.max_area || self.width * self.height > crate::CONFIG.media.max_area

View file

@ -4,12 +4,12 @@ use actix_web::web::Bytes;
use std::{ use std::{
future::Future, future::Future,
pin::Pin, pin::Pin,
process::{Stdio}, process::Stdio,
task::{Context, Poll}, task::{Context, Poll},
}; };
use tokio::{ use tokio::{
io::{AsyncRead, AsyncWriteExt, ReadBuf}, io::{AsyncRead, AsyncWriteExt, ReadBuf},
process::{Child, Command, ChildStdin}, process::{Child, ChildStdin, Command},
sync::oneshot::{channel, Receiver}, sync::oneshot::{channel, Receiver},
}; };
use tracing::{Instrument, Span}; use tracing::{Instrument, Span};
@ -83,7 +83,11 @@ impl Process {
self, self,
mut async_read: A, mut async_read: A,
) -> impl AsyncRead + Unpin { ) -> impl AsyncRead + Unpin {
self.read_fn(move |mut stdin| async move { tokio::io::copy(&mut async_read, &mut stdin).await.map(|_| ()) }) self.read_fn(move |mut stdin| async move {
tokio::io::copy(&mut async_read, &mut stdin)
.await
.map(|_| ())
})
} }
#[tracing::instrument] #[tracing::instrument]
@ -147,7 +151,6 @@ impl Process {
err_closed: false, err_closed: false,
handle: DropHandle { inner: handle }, handle: DropHandle { inner: handle },
} }
} }
} }

View file

@ -113,7 +113,11 @@ where
result result
} }
Err(e) => { Err(e) => {
tracing::warn!("Failed to ingest {}, {}", format!("{}", e), format!("{:?}", e)); tracing::warn!(
"Failed to ingest {}, {}",
format!("{}", e),
format!("{:?}", e)
);
UploadResult::Failure { UploadResult::Failure {
message: e.to_string(), message: e.to_string(),

View file

@ -44,11 +44,12 @@ pub(crate) async fn validate_image_bytes(
enable_full_video: bool, enable_full_video: bool,
validate: bool, validate: bool,
) -> Result<(ValidInputType, impl AsyncRead + Unpin), Error> { ) -> Result<(ValidInputType, impl AsyncRead + Unpin), Error> {
let input_type = if let Some(input_type) = crate::ffmpeg::input_type_bytes(bytes.clone()).await? { let input_type =
input_type.to_valid_input_type() if let Some(input_type) = crate::ffmpeg::input_type_bytes(bytes.clone()).await? {
} else { input_type
crate::magick::input_type_bytes(bytes.clone()).await? } else {
}; crate::magick::input_type_bytes(bytes.clone()).await?
};
if !validate { if !validate {
return Ok((input_type, Either::left(UnvalidatedBytes::new(bytes)))); return Ok((input_type, Either::left(UnvalidatedBytes::new(bytes))));
@ -84,7 +85,8 @@ pub(crate) async fn validate_image_bytes(
Ok(( Ok((
ValidInputType::Mp4, ValidInputType::Mp4,
Either::right(Either::left( Either::right(Either::left(
crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Webm, enable_full_video).await?, crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Webm, enable_full_video)
.await?,
)), )),
)) ))
} }