mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 19:31:35 +00:00
Add ffprobe for details inspection - vastly improve video detection speed
This commit is contained in:
parent
5449bb82f1
commit
80c83eb491
7 changed files with 206 additions and 71 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
137
src/ffmpeg.rs
137
src/ffmpeg.rs
|
@ -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,7 +217,9 @@ 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(
|
||||||
|
"ffmpeg",
|
||||||
|
&[
|
||||||
"-i",
|
"-i",
|
||||||
input_file_str,
|
input_file_str,
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
|
@ -136,9 +233,12 @@ pub(crate) async fn to_mp4_bytes(
|
||||||
"-f",
|
"-f",
|
||||||
"mp4",
|
"mp4",
|
||||||
output_file_str,
|
output_file_str,
|
||||||
])?
|
],
|
||||||
|
)?
|
||||||
} else {
|
} else {
|
||||||
Process::run("ffmpeg", &[
|
Process::run(
|
||||||
|
"ffmpeg",
|
||||||
|
&[
|
||||||
"-i",
|
"-i",
|
||||||
input_file_str,
|
input_file_str,
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
|
@ -151,7 +251,8 @@ pub(crate) async fn to_mp4_bytes(
|
||||||
"-f",
|
"-f",
|
||||||
"mp4",
|
"mp4",
|
||||||
output_file_str,
|
output_file_str,
|
||||||
])?
|
],
|
||||||
|
)?
|
||||||
};
|
};
|
||||||
|
|
||||||
process.wait().await?;
|
process.wait().await?;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 },
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -44,8 +44,9 @@ 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? {
|
||||||
|
input_type
|
||||||
} else {
|
} else {
|
||||||
crate::magick::input_type_bytes(bytes.clone()).await?
|
crate::magick::input_type_bytes(bytes.clone()).await?
|
||||||
};
|
};
|
||||||
|
@ -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?,
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue