2023-07-13 03:12:21 +00:00
|
|
|
use std::{collections::HashSet, sync::OnceLock};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
ffmpeg::FfMpegError,
|
2023-07-13 18:48:59 +00:00
|
|
|
formats::{
|
|
|
|
AnimationFormat, AnimationInput, ImageFormat, ImageInput, InputFile, InternalFormat,
|
|
|
|
InternalVideoFormat, VideoFormat,
|
|
|
|
},
|
2023-07-13 03:12:21 +00:00
|
|
|
process::Process,
|
|
|
|
};
|
|
|
|
use actix_web::web::Bytes;
|
2023-07-13 18:48:59 +00:00
|
|
|
use futures_util::Stream;
|
2023-07-13 03:12:21 +00:00
|
|
|
use tokio::io::AsyncReadExt;
|
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
use super::{Discovery, DiscoveryLite};
|
|
|
|
|
|
|
|
const FFMPEG_FORMAT_MAPPINGS: &[(&str, InternalFormat)] = &[
|
|
|
|
("apng", InternalFormat::Animation(AnimationFormat::Apng)),
|
|
|
|
("gif", InternalFormat::Animation(AnimationFormat::Gif)),
|
|
|
|
("mp4", InternalFormat::Video(InternalVideoFormat::Mp4)),
|
|
|
|
("png_pipe", InternalFormat::Image(ImageFormat::Png)),
|
|
|
|
("webm", InternalFormat::Video(InternalVideoFormat::Webm)),
|
|
|
|
("webp_pipe", InternalFormat::Image(ImageFormat::Webp)),
|
2023-07-13 03:12:21 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
|
|
struct FfMpegDiscovery {
|
|
|
|
streams: [FfMpegStream; 1],
|
|
|
|
format: FfMpegFormat,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
|
|
struct FfMpegStream {
|
|
|
|
width: u16,
|
|
|
|
height: u16,
|
|
|
|
nb_read_frames: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
|
|
struct FfMpegFormat {
|
|
|
|
format_name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(serde::Deserialize)]
|
|
|
|
struct PixelFormatOutput {
|
|
|
|
pixel_formats: Vec<PixelFormat>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(serde::Deserialize)]
|
|
|
|
struct PixelFormat {
|
|
|
|
name: String,
|
|
|
|
flags: Flags,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(serde::Deserialize)]
|
|
|
|
struct Flags {
|
|
|
|
alpha: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(super) async fn discover_bytes(bytes: Bytes) -> Result<Option<Discovery>, FfMpegError> {
|
2023-07-13 18:48:59 +00:00
|
|
|
discover_file_full(move |mut file| {
|
|
|
|
let bytes = bytes.clone();
|
|
|
|
|
|
|
|
async move {
|
|
|
|
file.write_from_bytes(bytes)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::Write)?;
|
|
|
|
Ok(file)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(super) async fn discover_bytes_lite(
|
|
|
|
bytes: Bytes,
|
|
|
|
) -> Result<Option<DiscoveryLite>, FfMpegError> {
|
|
|
|
discover_file_lite(move |mut file| async move {
|
2023-07-13 03:12:21 +00:00
|
|
|
file.write_from_bytes(bytes)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::Write)?;
|
|
|
|
Ok(file)
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
pub(super) async fn discover_stream_lite<S>(stream: S) -> Result<Option<DiscoveryLite>, FfMpegError>
|
|
|
|
where
|
|
|
|
S: Stream<Item = std::io::Result<Bytes>> + Unpin,
|
|
|
|
{
|
|
|
|
discover_file_lite(move |mut file| async move {
|
|
|
|
file.write_from_stream(stream)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::Write)?;
|
|
|
|
Ok(file)
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn discover_file_lite<F, Fut>(f: F) -> Result<Option<DiscoveryLite>, FfMpegError>
|
|
|
|
where
|
|
|
|
F: FnOnce(crate::file::File) -> Fut,
|
|
|
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
|
|
|
{
|
|
|
|
let Some(DiscoveryLite {
|
|
|
|
format,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
}) = discover_file(f)
|
|
|
|
.await? else {
|
|
|
|
return Ok(None);
|
|
|
|
};
|
|
|
|
|
|
|
|
// If we're not confident in our discovery don't return it
|
|
|
|
if width == 0 || height == 0 {
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Some(DiscoveryLite {
|
|
|
|
format,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn discover_file_full<F, Fut>(f: F) -> Result<Option<Discovery>, FfMpegError>
|
|
|
|
where
|
|
|
|
F: Fn(crate::file::File) -> Fut + Clone,
|
|
|
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
|
|
|
{
|
|
|
|
let Some(DiscoveryLite { format, width, height, frames }) = discover_file(f.clone()).await? else {
|
|
|
|
return Ok(None);
|
|
|
|
};
|
|
|
|
|
|
|
|
match format {
|
|
|
|
InternalFormat::Video(InternalVideoFormat::Webm) => {
|
|
|
|
static ALPHA_PIXEL_FORMATS: OnceLock<HashSet<String>> = OnceLock::new();
|
|
|
|
|
|
|
|
let format = pixel_format(f).await?;
|
|
|
|
|
|
|
|
let alpha = match ALPHA_PIXEL_FORMATS.get() {
|
|
|
|
Some(alpha_pixel_formats) => alpha_pixel_formats.contains(&format),
|
|
|
|
None => {
|
|
|
|
let pixel_formats = alpha_pixel_formats().await?;
|
|
|
|
let alpha = pixel_formats.contains(&format);
|
|
|
|
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
|
|
|
|
alpha
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(Some(Discovery {
|
|
|
|
input: InputFile::Video(VideoFormat::Webm { alpha }),
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
InternalFormat::Video(InternalVideoFormat::Mp4) => Ok(Some(Discovery {
|
|
|
|
input: InputFile::Video(VideoFormat::Mp4),
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
})),
|
|
|
|
InternalFormat::Animation(format) => Ok(Some(Discovery {
|
|
|
|
input: InputFile::Animation(AnimationInput { format }),
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
})),
|
|
|
|
InternalFormat::Image(format) => Ok(Some(Discovery {
|
|
|
|
input: InputFile::Image(ImageInput {
|
|
|
|
format,
|
|
|
|
needs_reorient: false,
|
|
|
|
}),
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
})),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-13 03:12:21 +00:00
|
|
|
#[tracing::instrument(skip(f))]
|
2023-07-13 18:48:59 +00:00
|
|
|
async fn discover_file<F, Fut>(f: F) -> Result<Option<DiscoveryLite>, FfMpegError>
|
2023-07-13 03:12:21 +00:00
|
|
|
where
|
|
|
|
F: FnOnce(crate::file::File) -> Fut,
|
|
|
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
|
|
|
{
|
|
|
|
let input_file = crate::tmp_file::tmp_file(None);
|
|
|
|
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&input_file)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::CreateDir)?;
|
|
|
|
|
|
|
|
let tmp_one = crate::file::File::create(&input_file)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::CreateFile)?;
|
|
|
|
let tmp_one = (f)(tmp_one).await?;
|
|
|
|
tmp_one.close().await.map_err(FfMpegError::CloseFile)?;
|
|
|
|
|
|
|
|
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",
|
|
|
|
"-print_format",
|
|
|
|
"json",
|
|
|
|
input_file_str,
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.map_err(FfMpegError::Process)?;
|
|
|
|
|
|
|
|
let mut output = Vec::new();
|
|
|
|
process
|
|
|
|
.read()
|
|
|
|
.read_to_end(&mut output)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::Read)?;
|
|
|
|
tokio::fs::remove_file(input_file_str)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::RemoveFile)?;
|
|
|
|
|
|
|
|
let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
parse_discovery_ffmpeg(output)
|
|
|
|
}
|
2023-07-13 03:12:21 +00:00
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
async fn pixel_format<F, Fut>(f: F) -> Result<String, FfMpegError>
|
|
|
|
where
|
|
|
|
F: FnOnce(crate::file::File) -> Fut,
|
|
|
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
|
|
|
{
|
|
|
|
let input_file = crate::tmp_file::tmp_file(None);
|
|
|
|
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&input_file)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::CreateDir)?;
|
2023-07-13 03:12:21 +00:00
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
let tmp_one = crate::file::File::create(&input_file)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::CreateFile)?;
|
|
|
|
let tmp_one = (f)(tmp_one).await?;
|
|
|
|
tmp_one.close().await.map_err(FfMpegError::CloseFile)?;
|
2023-07-13 03:12:21 +00:00
|
|
|
|
|
|
|
let process = Process::run(
|
|
|
|
"ffprobe",
|
|
|
|
&[
|
|
|
|
"-v",
|
|
|
|
"0",
|
|
|
|
"-select_streams",
|
|
|
|
"v:0",
|
|
|
|
"-show_entries",
|
|
|
|
"stream=pix_fmt",
|
|
|
|
"-of",
|
|
|
|
"compact=p=0:nk=1",
|
2023-07-13 18:48:59 +00:00
|
|
|
input_file_str,
|
2023-07-13 03:12:21 +00:00
|
|
|
],
|
|
|
|
)
|
|
|
|
.map_err(FfMpegError::Process)?;
|
|
|
|
|
|
|
|
let mut output = Vec::new();
|
|
|
|
process
|
|
|
|
.read()
|
|
|
|
.read_to_end(&mut output)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::Read)?;
|
2023-07-13 18:48:59 +00:00
|
|
|
|
|
|
|
tokio::fs::remove_file(input_file_str)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::RemoveFile)?;
|
|
|
|
|
2023-07-13 03:12:21 +00:00
|
|
|
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn alpha_pixel_formats() -> Result<HashSet<String>, FfMpegError> {
|
|
|
|
let process = Process::run(
|
|
|
|
"ffprobe",
|
|
|
|
&[
|
|
|
|
"-v",
|
|
|
|
"0",
|
|
|
|
"-show_entries",
|
|
|
|
"pixel_format=name:flags=alpha",
|
|
|
|
"-of",
|
|
|
|
"compact=p=0",
|
|
|
|
"-print_format",
|
|
|
|
"json",
|
|
|
|
],
|
|
|
|
)
|
|
|
|
.map_err(FfMpegError::Process)?;
|
|
|
|
|
|
|
|
let mut output = Vec::new();
|
|
|
|
process
|
|
|
|
.read()
|
|
|
|
.read_to_end(&mut output)
|
|
|
|
.await
|
|
|
|
.map_err(FfMpegError::Read)?;
|
|
|
|
|
|
|
|
let formats: PixelFormatOutput = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
|
|
|
|
|
|
|
Ok(parse_pixel_formats(formats))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet<String> {
|
|
|
|
formats
|
|
|
|
.pixel_formats
|
|
|
|
.into_iter()
|
|
|
|
.filter_map(|PixelFormat { name, flags }| {
|
|
|
|
if flags.alpha == 0 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(name)
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
fn parse_discovery_ffmpeg(
|
|
|
|
discovery: FfMpegDiscovery,
|
|
|
|
) -> Result<Option<DiscoveryLite>, FfMpegError> {
|
2023-07-13 03:12:21 +00:00
|
|
|
let FfMpegDiscovery {
|
|
|
|
streams:
|
|
|
|
[FfMpegStream {
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
nb_read_frames,
|
|
|
|
}],
|
|
|
|
format: FfMpegFormat { format_name },
|
|
|
|
} = discovery;
|
|
|
|
|
|
|
|
if let Some((name, value)) = FFMPEG_FORMAT_MAPPINGS
|
|
|
|
.iter()
|
|
|
|
.find(|(name, _)| format_name.contains(name))
|
|
|
|
{
|
|
|
|
let frames = nb_read_frames.and_then(|frames| frames.parse().ok());
|
|
|
|
|
|
|
|
if *name == "mp4" && frames.map(|nb| nb == 1).unwrap_or(false) {
|
|
|
|
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed mp4 even when
|
|
|
|
// animated
|
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
return Ok(Some(DiscoveryLite {
|
|
|
|
format: InternalFormat::Animation(AnimationFormat::Avif),
|
2023-07-13 03:12:21 +00:00
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
if *name == "webp" && (frames.is_none() || width == 0 || height == 0) {
|
|
|
|
// Might be Animated Webp, ffmpeg incorrectly detects animated webp as having no frames
|
|
|
|
// and 0 dimensions
|
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
return Ok(Some(DiscoveryLite {
|
|
|
|
format: InternalFormat::Animation(AnimationFormat::Webp),
|
2023-07-13 03:12:21 +00:00
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2023-07-13 18:48:59 +00:00
|
|
|
return Ok(Some(DiscoveryLite {
|
|
|
|
format: *value,
|
2023-07-13 03:12:21 +00:00
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames,
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
tracing::info!("No matching format mapping for {format_name}");
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|