mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 11:21:24 +00:00
Remove transcode from animation to video, make video transcoding 'optional'
Video transcoding still happens, but in many cases the video stream is able to be copied verbatim rather than being decoded & encoded
This commit is contained in:
parent
08fd96c2f7
commit
b48a9233b2
26 changed files with 858 additions and 716 deletions
|
@ -1,6 +1,5 @@
|
|||
use crate::{
|
||||
config::primitives::{LogFormat, Targets},
|
||||
formats::VideoCodec,
|
||||
serde_str::Serde,
|
||||
};
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
@ -120,7 +119,6 @@ struct VideoDefaults {
|
|||
max_area: u32,
|
||||
max_frame_count: u32,
|
||||
max_file_size: usize,
|
||||
video_codec: VideoCodec,
|
||||
quality: VideoQualityDefaults,
|
||||
}
|
||||
|
||||
|
@ -283,7 +281,6 @@ impl Default for VideoDefaults {
|
|||
max_area: 8_294_400,
|
||||
max_frame_count: 900,
|
||||
max_file_size: 40,
|
||||
video_codec: VideoCodec::Vp9,
|
||||
quality: VideoQualityDefaults::default(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -300,7 +300,8 @@ pub(crate) struct Video {
|
|||
|
||||
pub(crate) max_frame_count: u32,
|
||||
|
||||
pub(crate) video_codec: VideoCodec,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) video_codec: Option<VideoCodec>,
|
||||
|
||||
pub(crate) quality: VideoQuality,
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use crate::{
|
||||
discover::DiscoveryLite,
|
||||
bytes_stream::BytesStream,
|
||||
discover::Discovery,
|
||||
error::Error,
|
||||
formats::{InternalFormat, InternalVideoFormat},
|
||||
serde_str::Serde,
|
||||
store::Store,
|
||||
stream::IntoStreamer,
|
||||
};
|
||||
use actix_web::web;
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
|
@ -35,14 +37,19 @@ impl Details {
|
|||
}
|
||||
|
||||
pub(crate) async fn from_bytes(timeout: u64, input: web::Bytes) -> Result<Self, Error> {
|
||||
let DiscoveryLite {
|
||||
format,
|
||||
let Discovery {
|
||||
input,
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
} = crate::discover::discover_bytes_lite(timeout, input).await?;
|
||||
} = crate::discover::discover_bytes(timeout, input).await?;
|
||||
|
||||
Ok(Details::from_parts(format, width, height, frames))
|
||||
Ok(Details::from_parts(
|
||||
input.internal_format(),
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn from_store<S: Store>(
|
||||
|
@ -50,14 +57,20 @@ impl Details {
|
|||
identifier: &S::Identifier,
|
||||
timeout: u64,
|
||||
) -> Result<Self, Error> {
|
||||
let DiscoveryLite {
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
} = crate::discover::discover_store_lite(store, identifier, timeout).await?;
|
||||
let mut buf = BytesStream::new();
|
||||
|
||||
Ok(Details::from_parts(format, width, height, frames))
|
||||
let mut stream = store
|
||||
.to_stream(identifier, None, None)
|
||||
.await?
|
||||
.into_streamer();
|
||||
|
||||
while let Some(res) = stream.next().await {
|
||||
buf.add_bytes(res?);
|
||||
}
|
||||
|
||||
let bytes = buf.into_bytes();
|
||||
|
||||
Self::from_bytes(timeout, bytes).await
|
||||
}
|
||||
|
||||
pub(crate) fn internal_format(&self) -> InternalFormat {
|
||||
|
|
|
@ -4,10 +4,7 @@ mod magick;
|
|||
|
||||
use actix_web::web::Bytes;
|
||||
|
||||
use crate::{
|
||||
formats::{InputFile, InternalFormat},
|
||||
store::Store,
|
||||
};
|
||||
use crate::formats::InputFile;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Discovery {
|
||||
|
@ -17,14 +14,6 @@ pub(crate) struct Discovery {
|
|||
pub(crate) frames: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct DiscoveryLite {
|
||||
pub(crate) format: InternalFormat,
|
||||
pub(crate) width: u16,
|
||||
pub(crate) height: u16,
|
||||
pub(crate) frames: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum DiscoverError {
|
||||
#[error("No frames in uploaded media")]
|
||||
|
@ -37,41 +26,6 @@ pub(crate) enum DiscoverError {
|
|||
UnsupportedFileType(String),
|
||||
}
|
||||
|
||||
pub(crate) async fn discover_bytes_lite(
|
||||
timeout: u64,
|
||||
bytes: Bytes,
|
||||
) -> Result<DiscoveryLite, crate::error::Error> {
|
||||
if let Some(discovery) = ffmpeg::discover_bytes_lite(timeout, bytes.clone()).await? {
|
||||
return Ok(discovery);
|
||||
}
|
||||
|
||||
let discovery = magick::discover_bytes_lite(timeout, bytes).await?;
|
||||
|
||||
Ok(discovery)
|
||||
}
|
||||
|
||||
pub(crate) async fn discover_store_lite<S>(
|
||||
store: &S,
|
||||
identifier: &S::Identifier,
|
||||
timeout: u64,
|
||||
) -> Result<DiscoveryLite, crate::error::Error>
|
||||
where
|
||||
S: Store,
|
||||
{
|
||||
if let Some(discovery) =
|
||||
ffmpeg::discover_stream_lite(timeout, store.to_stream(identifier, None, None).await?)
|
||||
.await?
|
||||
{
|
||||
return Ok(discovery);
|
||||
}
|
||||
|
||||
let discovery =
|
||||
magick::discover_stream_lite(timeout, store.to_stream(identifier, None, None).await?)
|
||||
.await?;
|
||||
|
||||
Ok(discovery)
|
||||
}
|
||||
|
||||
pub(crate) async fn discover_bytes(
|
||||
timeout: u64,
|
||||
bytes: Bytes,
|
||||
|
|
|
@ -6,40 +6,134 @@ use std::{collections::HashSet, sync::OnceLock};
|
|||
use crate::{
|
||||
ffmpeg::FfMpegError,
|
||||
formats::{
|
||||
AnimationFormat, ImageFormat, ImageInput, InputFile, InternalFormat, InternalVideoFormat,
|
||||
VideoFormat,
|
||||
AlphaCodec, AnimationFormat, ImageFormat, ImageInput, InputFile, InputVideoFormat,
|
||||
Mp4AudioCodec, Mp4Codec, WebmAlphaCodec, WebmAudioCodec, WebmCodec,
|
||||
},
|
||||
process::Process,
|
||||
};
|
||||
use actix_web::web::Bytes;
|
||||
use futures_core::Stream;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::{Discovery, DiscoveryLite};
|
||||
use super::Discovery;
|
||||
|
||||
const MP4: &str = "mp4";
|
||||
const WEBP: &str = "webp_pipe";
|
||||
|
||||
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, InternalFormat::Image(ImageFormat::Webp)),
|
||||
];
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct FfMpegDiscovery {
|
||||
streams: [FfMpegStream; 1],
|
||||
streams: FfMpegStreams,
|
||||
format: FfMpegFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct FfMpegStream {
|
||||
#[serde(transparent)]
|
||||
struct FfMpegStreams {
|
||||
streams: Vec<FfMpegStream>,
|
||||
}
|
||||
|
||||
impl FfMpegStreams {
|
||||
fn into_parts(self) -> Option<(FfMpegVideoStream, Option<FfMpegAudioStream>)> {
|
||||
let mut video = None;
|
||||
let mut audio = None;
|
||||
|
||||
for stream in self.streams {
|
||||
match stream {
|
||||
FfMpegStream::Video(video_stream) if video.is_none() => {
|
||||
video = Some(video_stream);
|
||||
}
|
||||
FfMpegStream::Audio(audio_stream) if audio.is_none() => {
|
||||
audio = Some(audio_stream);
|
||||
}
|
||||
FfMpegStream::Video(FfMpegVideoStream { codec_name, .. }) => {
|
||||
tracing::info!("Encountered duplicate video stream {codec_name:?}");
|
||||
}
|
||||
FfMpegStream::Audio(FfMpegAudioStream { codec_name, .. }) => {
|
||||
tracing::info!("Encountered duplicate audio stream {codec_name:?}");
|
||||
}
|
||||
FfMpegStream::Unknown { codec_name } => {
|
||||
tracing::info!("Encountered unknown stream {codec_name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video.map(|v| (v, audio))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
enum FfMpegVideoCodec {
|
||||
#[serde(rename = "apng")]
|
||||
Apng,
|
||||
#[serde(rename = "av1")]
|
||||
Av1, // still or animated avif, or av1 video
|
||||
#[serde(rename = "gif")]
|
||||
Gif,
|
||||
#[serde(rename = "h264")]
|
||||
H264,
|
||||
#[serde(rename = "hevc")]
|
||||
Hevc, // h265 video
|
||||
#[serde(rename = "mjpeg")]
|
||||
Mjpeg,
|
||||
#[serde(rename = "jpegxl")]
|
||||
Jpegxl,
|
||||
#[serde(rename = "png")]
|
||||
Png,
|
||||
#[serde(rename = "vp8")]
|
||||
Vp8,
|
||||
#[serde(rename = "vp9")]
|
||||
Vp9,
|
||||
#[serde(rename = "webp")]
|
||||
Webp,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
enum FfMpegAudioCodec {
|
||||
#[serde(rename = "aac")]
|
||||
Aac,
|
||||
#[serde(rename = "opus")]
|
||||
Opus,
|
||||
#[serde(rename = "vorbis")]
|
||||
Vorbis,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FrameString {
|
||||
frames: u32,
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for FrameString {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
let frames = String::deserialize(deserializer)?
|
||||
.parse()
|
||||
.map_err(|_| D::Error::custom("Invalid frames string"))?;
|
||||
|
||||
Ok(FrameString { frames })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct FfMpegAudioStream {
|
||||
codec_name: FfMpegAudioCodec,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct FfMpegVideoStream {
|
||||
codec_name: FfMpegVideoCodec,
|
||||
width: u16,
|
||||
height: u16,
|
||||
nb_read_frames: Option<String>,
|
||||
pix_fmt: Option<String>,
|
||||
nb_read_frames: Option<FrameString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum FfMpegStream {
|
||||
Audio(FfMpegAudioStream),
|
||||
Video(FfMpegVideoStream),
|
||||
Unknown { codec_name: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
|
@ -67,7 +161,7 @@ pub(super) async fn discover_bytes(
|
|||
timeout: u64,
|
||||
bytes: Bytes,
|
||||
) -> Result<Option<Discovery>, FfMpegError> {
|
||||
discover_file_full(
|
||||
discover_file(
|
||||
move |mut file| {
|
||||
let bytes = bytes.clone();
|
||||
|
||||
|
@ -83,130 +177,22 @@ pub(super) async fn discover_bytes(
|
|||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn discover_bytes_lite(
|
||||
timeout: u64,
|
||||
bytes: Bytes,
|
||||
) -> Result<Option<DiscoveryLite>, FfMpegError> {
|
||||
discover_file_lite(
|
||||
move |mut file| async move {
|
||||
file.write_from_bytes(bytes)
|
||||
.await
|
||||
.map_err(FfMpegError::Write)?;
|
||||
Ok(file)
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.await
|
||||
}
|
||||
async fn allows_alpha(pixel_format: &str, timeout: u64) -> Result<bool, FfMpegError> {
|
||||
static ALPHA_PIXEL_FORMATS: OnceLock<HashSet<String>> = OnceLock::new();
|
||||
|
||||
pub(super) async fn discover_stream_lite<S>(
|
||||
timeout: u64,
|
||||
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)
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn discover_file_lite<F, Fut>(
|
||||
f: F,
|
||||
timeout: u64,
|
||||
) -> 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, timeout)
|
||||
.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, timeout: u64) -> 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(), timeout).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, timeout).await?;
|
||||
|
||||
let alpha = match ALPHA_PIXEL_FORMATS.get() {
|
||||
Some(alpha_pixel_formats) => alpha_pixel_formats.contains(&format),
|
||||
None => {
|
||||
let pixel_formats = alpha_pixel_formats(timeout).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,
|
||||
}))
|
||||
match ALPHA_PIXEL_FORMATS.get() {
|
||||
Some(alpha_pixel_formats) => Ok(alpha_pixel_formats.contains(pixel_format)),
|
||||
None => {
|
||||
let pixel_formats = alpha_pixel_formats(timeout).await?;
|
||||
let alpha = pixel_formats.contains(pixel_format);
|
||||
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
|
||||
Ok(alpha)
|
||||
}
|
||||
InternalFormat::Video(InternalVideoFormat::Mp4) => Ok(Some(Discovery {
|
||||
input: InputFile::Video(VideoFormat::Mp4),
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
})),
|
||||
InternalFormat::Animation(format) => Ok(Some(Discovery {
|
||||
input: InputFile::Animation(format),
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
})),
|
||||
InternalFormat::Image(format) => Ok(Some(Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(f))]
|
||||
async fn discover_file<F, Fut>(f: F, timeout: u64) -> Result<Option<DiscoveryLite>, FfMpegError>
|
||||
async fn discover_file<F, Fut>(f: F, timeout: u64) -> Result<Option<Discovery>, FfMpegError>
|
||||
where
|
||||
F: FnOnce(crate::file::File) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
||||
|
@ -228,11 +214,9 @@ where
|
|||
&[
|
||||
"-v",
|
||||
"quiet",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-count_frames",
|
||||
"-show_entries",
|
||||
"stream=width,height,nb_read_frames:format=format_name",
|
||||
"stream=width,height,nb_read_frames,codec_name,pix_fmt:format=format_name",
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
"-print_format",
|
||||
|
@ -254,54 +238,23 @@ where
|
|||
|
||||
let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
||||
|
||||
parse_discovery(output)
|
||||
}
|
||||
let (discovery, pix_fmt) = parse_discovery(output)?;
|
||||
|
||||
async fn pixel_format<F, Fut>(f: F, timeout: u64) -> 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)?;
|
||||
let Some(mut discovery) = discovery else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
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)?;
|
||||
if let Some(pixel_format) = pix_fmt {
|
||||
if let InputFile::Video(InputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec { alpha, .. }),
|
||||
..
|
||||
}) = &mut discovery.input
|
||||
{
|
||||
*alpha = allows_alpha(&pixel_format, timeout).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let process = Process::run(
|
||||
"ffprobe",
|
||||
&[
|
||||
"-v",
|
||||
"0",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=pix_fmt",
|
||||
"-of",
|
||||
"compact=p=0:nk=1",
|
||||
input_file_str,
|
||||
],
|
||||
timeout,
|
||||
)?;
|
||||
|
||||
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)?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
||||
Ok(Some(discovery))
|
||||
}
|
||||
|
||||
async fn alpha_pixel_formats(timeout: u64) -> Result<HashSet<String>, FfMpegError> {
|
||||
|
@ -346,56 +299,145 @@ fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet<String> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn parse_discovery(discovery: FfMpegDiscovery) -> Result<Option<DiscoveryLite>, FfMpegError> {
|
||||
fn is_mp4(format_name: &str) -> bool {
|
||||
format_name.contains(MP4)
|
||||
}
|
||||
|
||||
fn mp4_audio_codec(stream: Option<FfMpegAudioStream>) -> Option<Mp4AudioCodec> {
|
||||
match stream {
|
||||
Some(FfMpegAudioStream {
|
||||
codec_name: FfMpegAudioCodec::Aac,
|
||||
}) => Some(Mp4AudioCodec::Aac),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn webm_audio_codec(stream: Option<FfMpegAudioStream>) -> Option<WebmAudioCodec> {
|
||||
match stream {
|
||||
Some(FfMpegAudioStream {
|
||||
codec_name: FfMpegAudioCodec::Opus,
|
||||
}) => Some(WebmAudioCodec::Opus),
|
||||
Some(FfMpegAudioStream {
|
||||
codec_name: FfMpegAudioCodec::Vorbis,
|
||||
}) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_discovery(
|
||||
discovery: FfMpegDiscovery,
|
||||
) -> Result<(Option<Discovery>, Option<String>), FfMpegError> {
|
||||
let FfMpegDiscovery {
|
||||
streams:
|
||||
[FfMpegStream {
|
||||
width,
|
||||
height,
|
||||
nb_read_frames,
|
||||
}],
|
||||
streams,
|
||||
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());
|
||||
let Some((video_stream, audio_stream)) = streams.into_parts() else {
|
||||
tracing::info!("No matching format mapping for {format_name}");
|
||||
return Ok((None, None));
|
||||
};
|
||||
|
||||
if *name == MP4 && frames.map(|nb| nb == 1).unwrap_or(false) {
|
||||
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed mp4 even when
|
||||
let input = match video_stream.codec_name {
|
||||
FfMpegVideoCodec::Av1
|
||||
if video_stream
|
||||
.nb_read_frames
|
||||
.as_ref()
|
||||
.is_some_and(|count| count.frames == 1) =>
|
||||
{
|
||||
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed av1 even when
|
||||
// animated
|
||||
|
||||
return Ok(Some(DiscoveryLite {
|
||||
format: InternalFormat::Animation(AnimationFormat::Avif),
|
||||
width,
|
||||
height,
|
||||
frames: None,
|
||||
}));
|
||||
return Ok((
|
||||
Some(Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Avif),
|
||||
width: video_stream.width,
|
||||
height: video_stream.height,
|
||||
frames: None,
|
||||
}),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
if *name == WEBP && (frames.is_none() || width == 0 || height == 0) {
|
||||
FfMpegVideoCodec::Webp
|
||||
if video_stream.height == 0
|
||||
|| video_stream.width == 0
|
||||
|| video_stream.nb_read_frames.is_none() =>
|
||||
{
|
||||
// Might be Animated Webp, ffmpeg incorrectly detects animated webp as having no frames
|
||||
// and 0 dimensions
|
||||
|
||||
return Ok(Some(DiscoveryLite {
|
||||
format: InternalFormat::Animation(AnimationFormat::Webp),
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
}));
|
||||
return Ok((
|
||||
Some(Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Webp),
|
||||
width: video_stream.width,
|
||||
height: video_stream.height,
|
||||
frames: None,
|
||||
}),
|
||||
None,
|
||||
));
|
||||
}
|
||||
FfMpegVideoCodec::Av1 if is_mp4(&format_name) => InputFile::Video(InputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::Av1,
|
||||
audio_codec: mp4_audio_codec(audio_stream),
|
||||
}),
|
||||
FfMpegVideoCodec::Av1 => InputFile::Video(InputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec: webm_audio_codec(audio_stream),
|
||||
}),
|
||||
FfMpegVideoCodec::Apng => InputFile::Animation(AnimationFormat::Apng),
|
||||
FfMpegVideoCodec::Gif => InputFile::Animation(AnimationFormat::Gif),
|
||||
FfMpegVideoCodec::H264 => InputFile::Video(InputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H264,
|
||||
audio_codec: mp4_audio_codec(audio_stream),
|
||||
}),
|
||||
FfMpegVideoCodec::Hevc => InputFile::Video(InputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H265,
|
||||
audio_codec: mp4_audio_codec(audio_stream),
|
||||
}),
|
||||
FfMpegVideoCodec::Png => InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Png,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
FfMpegVideoCodec::Mjpeg => InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Jpeg,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
FfMpegVideoCodec::Jpegxl => InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Jxl,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
FfMpegVideoCodec::Vp8 => InputFile::Video(InputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp8,
|
||||
}),
|
||||
audio_codec: webm_audio_codec(audio_stream),
|
||||
}),
|
||||
FfMpegVideoCodec::Vp9 => InputFile::Video(InputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: webm_audio_codec(audio_stream),
|
||||
}),
|
||||
FfMpegVideoCodec::Webp => InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Webp,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
};
|
||||
|
||||
return Ok(Some(DiscoveryLite {
|
||||
format: *value,
|
||||
width,
|
||||
height,
|
||||
frames: frames.and_then(|frames| if frames > 1 { Some(frames) } else { None }),
|
||||
}));
|
||||
}
|
||||
|
||||
tracing::info!("No matching format mapping for {format_name}");
|
||||
|
||||
Ok(None)
|
||||
Ok((
|
||||
Some(Discovery {
|
||||
input,
|
||||
width: video_stream.width,
|
||||
height: video_stream.height,
|
||||
frames: video_stream.nb_read_frames.and_then(|f| {
|
||||
if f.frames <= 1 {
|
||||
None
|
||||
} else {
|
||||
Some(f.frames)
|
||||
}
|
||||
}),
|
||||
}),
|
||||
video_stream.pix_fmt,
|
||||
))
|
||||
}
|
||||
|
|
17
src/discover/ffmpeg/ffprobe_6_0_animated_avif_details.json
Normal file
17
src/discover/ffmpeg/ffprobe_6_0_animated_avif_details.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"programs": [
|
||||
|
||||
],
|
||||
"streams": [
|
||||
{
|
||||
"codec_name": "av1",
|
||||
"width": 112,
|
||||
"height": 112,
|
||||
"pix_fmt": "yuv420p",
|
||||
"nb_read_frames": "1"
|
||||
}
|
||||
],
|
||||
"format": {
|
||||
"format_name": "mov,mp4,m4a,3gp,3g2,mj2"
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"codec_name": "webp",
|
||||
"width": 0,
|
||||
"height": 0
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"codec_name": "apng",
|
||||
"width": 112,
|
||||
"height": 112,
|
||||
"pix_fmt": "rgba",
|
||||
"nb_read_frames": "27"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"codec_name": "av1",
|
||||
"width": 1200,
|
||||
"height": 1387,
|
||||
"pix_fmt": "yuv420p",
|
||||
"nb_read_frames": "1"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"width": 160,
|
||||
"height": 227,
|
||||
"nb_read_frames": "28"
|
||||
"codec_name": "gif",
|
||||
"width": 112,
|
||||
"height": 112,
|
||||
"pix_fmt": "bgra",
|
||||
"nb_read_frames": "27"
|
||||
}
|
||||
],
|
||||
"format": {
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"codec_name": "mjpeg",
|
||||
"width": 1663,
|
||||
"height": 1247,
|
||||
"pix_fmt": "yuvj420p",
|
||||
"nb_read_frames": "1"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"codec_name": "jpegxl",
|
||||
"width": 0,
|
||||
"height": 0
|
||||
}
|
||||
|
|
17
src/discover/ffmpeg/ffprobe_6_0_mp4_av1_details.json
Normal file
17
src/discover/ffmpeg/ffprobe_6_0_mp4_av1_details.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"programs": [
|
||||
|
||||
],
|
||||
"streams": [
|
||||
{
|
||||
"codec_name": "av1",
|
||||
"width": 112,
|
||||
"height": 112,
|
||||
"pix_fmt": "yuv420p",
|
||||
"nb_read_frames": "27"
|
||||
}
|
||||
],
|
||||
"format": {
|
||||
"format_name": "mov,mp4,m4a,3gp,3g2,mj2"
|
||||
}
|
||||
}
|
|
@ -4,9 +4,11 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"width": 852,
|
||||
"height": 480,
|
||||
"nb_read_frames": "35364"
|
||||
"codec_name": "h264",
|
||||
"width": 1426,
|
||||
"height": 834,
|
||||
"pix_fmt": "yuv420p",
|
||||
"nb_read_frames": "105"
|
||||
}
|
||||
],
|
||||
"format": {
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"codec_name": "png",
|
||||
"width": 450,
|
||||
"height": 401,
|
||||
"pix_fmt": "rgb24",
|
||||
"nb_read_frames": "1"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"codec_name": "av1",
|
||||
"width": 112,
|
||||
"height": 112,
|
||||
"pix_fmt": "gbrp",
|
||||
"nb_read_frames": "27"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"width": 640,
|
||||
"height": 480,
|
||||
"nb_read_frames": "34650"
|
||||
"codec_name": "vp9",
|
||||
"width": 112,
|
||||
"height": 112,
|
||||
"pix_fmt": "yuv420p",
|
||||
"nb_read_frames": "27"
|
||||
}
|
||||
],
|
||||
"format": {
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
],
|
||||
"streams": [
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"codec_name": "webp",
|
||||
"width": 1200,
|
||||
"height": 1387,
|
||||
"pix_fmt": "yuv420p",
|
||||
"nb_read_frames": "1"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -1,22 +1,34 @@
|
|||
use crate::formats::{AnimationFormat, ImageFormat, InternalFormat, InternalVideoFormat};
|
||||
use crate::formats::{
|
||||
AlphaCodec, AnimationFormat, ImageFormat, ImageInput, InputFile, InputVideoFormat, Mp4Codec,
|
||||
WebmAlphaCodec, WebmCodec,
|
||||
};
|
||||
|
||||
use super::{DiscoveryLite, FfMpegDiscovery, PixelFormatOutput};
|
||||
use super::{Discovery, FfMpegDiscovery, PixelFormatOutput};
|
||||
|
||||
fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
|
||||
fn details_tests() -> [(&'static str, Option<Discovery>); 13] {
|
||||
[
|
||||
(
|
||||
"animated_webp",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Animation(AnimationFormat::Webp),
|
||||
Some(Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Webp),
|
||||
width: 0,
|
||||
height: 0,
|
||||
frames: None,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"animated_avif",
|
||||
Some(Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Avif),
|
||||
width: 112,
|
||||
height: 112,
|
||||
frames: None,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"apng",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Animation(AnimationFormat::Apng),
|
||||
Some(Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Apng),
|
||||
width: 112,
|
||||
height: 112,
|
||||
frames: Some(27),
|
||||
|
@ -24,37 +36,77 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
|
|||
),
|
||||
(
|
||||
"avif",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Animation(AnimationFormat::Avif),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
Some(Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Avif),
|
||||
width: 1200,
|
||||
height: 1387,
|
||||
frames: None,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"gif",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Animation(AnimationFormat::Gif),
|
||||
width: 160,
|
||||
height: 227,
|
||||
frames: Some(28),
|
||||
Some(Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Gif),
|
||||
width: 112,
|
||||
height: 112,
|
||||
frames: Some(27),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"jpeg",
|
||||
Some(Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Jpeg,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 1663,
|
||||
height: 1247,
|
||||
frames: None,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"jxl",
|
||||
Some(Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Jxl,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 0,
|
||||
height: 0,
|
||||
frames: None,
|
||||
}),
|
||||
),
|
||||
("jpeg", None),
|
||||
("jxl", None),
|
||||
(
|
||||
"mp4",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Video(InternalVideoFormat::Mp4),
|
||||
width: 852,
|
||||
height: 480,
|
||||
frames: Some(35364),
|
||||
Some(Discovery {
|
||||
input: InputFile::Video(InputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H264,
|
||||
audio_codec: None,
|
||||
}),
|
||||
width: 1426,
|
||||
height: 834,
|
||||
frames: Some(105),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"mp4_av1",
|
||||
Some(Discovery {
|
||||
input: InputFile::Video(InputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::Av1,
|
||||
audio_codec: None,
|
||||
}),
|
||||
width: 112,
|
||||
height: 112,
|
||||
frames: Some(27),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"png",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Image(ImageFormat::Png),
|
||||
Some(Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Png,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 450,
|
||||
height: 401,
|
||||
frames: None,
|
||||
|
@ -62,17 +114,26 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
|
|||
),
|
||||
(
|
||||
"webm",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Video(InternalVideoFormat::Webm),
|
||||
width: 640,
|
||||
height: 480,
|
||||
frames: Some(34650),
|
||||
Some(Discovery {
|
||||
input: InputFile::Video(InputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: None,
|
||||
}),
|
||||
width: 112,
|
||||
height: 112,
|
||||
frames: Some(27),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"webm_av1",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Video(InternalVideoFormat::Webm),
|
||||
Some(Discovery {
|
||||
input: InputFile::Video(InputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec: None,
|
||||
}),
|
||||
width: 112,
|
||||
height: 112,
|
||||
frames: Some(27),
|
||||
|
@ -80,10 +141,13 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
|
|||
),
|
||||
(
|
||||
"webp",
|
||||
Some(DiscoveryLite {
|
||||
format: InternalFormat::Image(ImageFormat::Webp),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
Some(Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Webp,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 1200,
|
||||
height: 1387,
|
||||
frames: None,
|
||||
}),
|
||||
),
|
||||
|
@ -100,7 +164,7 @@ fn parse_discovery() {
|
|||
|
||||
let json: FfMpegDiscovery = serde_json::from_str(&string).expect("Valid json");
|
||||
|
||||
let output = super::parse_discovery(json).expect("Parsed details");
|
||||
let (output, _) = super::parse_discovery(json).expect("Parsed details");
|
||||
|
||||
assert_eq!(output, expected);
|
||||
}
|
||||
|
|
|
@ -2,17 +2,16 @@
|
|||
mod tests;
|
||||
|
||||
use actix_web::web::Bytes;
|
||||
use futures_core::Stream;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::{
|
||||
discover::DiscoverError,
|
||||
formats::{AnimationFormat, ImageFormat, ImageInput, InputFile, VideoFormat},
|
||||
formats::{AnimationFormat, ImageFormat, ImageInput, InputFile},
|
||||
magick::MagickError,
|
||||
process::Process,
|
||||
};
|
||||
|
||||
use super::{Discovery, DiscoveryLite};
|
||||
use super::Discovery;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct MagickDiscovery {
|
||||
|
@ -31,59 +30,6 @@ struct Geometry {
|
|||
height: u16,
|
||||
}
|
||||
|
||||
impl Discovery {
|
||||
fn lite(self) -> DiscoveryLite {
|
||||
let Discovery {
|
||||
input,
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
} = self;
|
||||
|
||||
DiscoveryLite {
|
||||
format: input.internal_format(),
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn discover_bytes_lite(
|
||||
timeout: u64,
|
||||
bytes: Bytes,
|
||||
) -> Result<DiscoveryLite, MagickError> {
|
||||
discover_file_lite(
|
||||
move |mut file| async move {
|
||||
file.write_from_bytes(bytes)
|
||||
.await
|
||||
.map_err(MagickError::Write)?;
|
||||
Ok(file)
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn discover_stream_lite<S>(
|
||||
timeout: u64,
|
||||
stream: S,
|
||||
) -> Result<DiscoveryLite, MagickError>
|
||||
where
|
||||
S: Stream<Item = std::io::Result<Bytes>> + Unpin + 'static,
|
||||
{
|
||||
discover_file_lite(
|
||||
move |mut file| async move {
|
||||
file.write_from_stream(stream)
|
||||
.await
|
||||
.map_err(MagickError::Write)?;
|
||||
Ok(file)
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn confirm_bytes(
|
||||
discovery: Option<Discovery>,
|
||||
timeout: u64,
|
||||
|
@ -107,6 +53,18 @@ pub(super) async fn confirm_bytes(
|
|||
)
|
||||
.await?;
|
||||
|
||||
if frames == 1 {
|
||||
return Ok(Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Avif,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width,
|
||||
height,
|
||||
frames: None,
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Avif),
|
||||
width,
|
||||
|
@ -189,14 +147,6 @@ where
|
|||
Ok(lines)
|
||||
}
|
||||
|
||||
async fn discover_file_lite<F, Fut>(f: F, timeout: u64) -> Result<DiscoveryLite, MagickError>
|
||||
where
|
||||
F: FnOnce(crate::file::File) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<crate::file::File, MagickError>>,
|
||||
{
|
||||
discover_file(f, timeout).await.map(Discovery::lite)
|
||||
}
|
||||
|
||||
async fn discover_file<F, Fut>(f: F, timeout: u64) -> Result<Discovery, MagickError>
|
||||
where
|
||||
F: FnOnce(crate::file::File) -> Fut,
|
||||
|
@ -338,12 +288,6 @@ fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, DiscoverEr
|
|||
height,
|
||||
frames: None,
|
||||
}),
|
||||
"MP4" => Ok(Discovery {
|
||||
input: InputFile::Video(VideoFormat::Mp4),
|
||||
width,
|
||||
height,
|
||||
frames: Some(frames),
|
||||
}),
|
||||
"PNG" => Ok(Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Png,
|
||||
|
@ -373,12 +317,6 @@ fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, DiscoverEr
|
|||
})
|
||||
}
|
||||
}
|
||||
"WEBM" => Ok(Discovery {
|
||||
input: InputFile::Video(VideoFormat::Webm { alpha: true }),
|
||||
width,
|
||||
height,
|
||||
frames: Some(frames),
|
||||
}),
|
||||
otherwise => Err(DiscoverError::UnsupportedFileType(String::from(otherwise))),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use crate::formats::{AnimationFormat, ImageFormat, InternalFormat, InternalVideoFormat};
|
||||
use crate::formats::{AnimationFormat, ImageFormat, ImageInput, InputFile};
|
||||
|
||||
use super::{DiscoveryLite, MagickDiscovery};
|
||||
use super::{Discovery, MagickDiscovery};
|
||||
|
||||
fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
||||
fn details_tests() -> [(&'static str, Discovery); 7] {
|
||||
[
|
||||
(
|
||||
"animated_webp",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Animation(AnimationFormat::Webp),
|
||||
Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Webp),
|
||||
width: 112,
|
||||
height: 112,
|
||||
frames: Some(27),
|
||||
|
@ -15,8 +15,11 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
|||
),
|
||||
(
|
||||
"avif",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Image(ImageFormat::Avif),
|
||||
Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Avif,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frames: None,
|
||||
|
@ -24,8 +27,8 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
|||
),
|
||||
(
|
||||
"gif",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Animation(AnimationFormat::Gif),
|
||||
Discovery {
|
||||
input: InputFile::Animation(AnimationFormat::Gif),
|
||||
width: 414,
|
||||
height: 261,
|
||||
frames: Some(17),
|
||||
|
@ -33,8 +36,11 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
|||
),
|
||||
(
|
||||
"jpeg",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Image(ImageFormat::Jpeg),
|
||||
Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Jpeg,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frames: None,
|
||||
|
@ -42,44 +48,35 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
|||
),
|
||||
(
|
||||
"jxl",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Image(ImageFormat::Jxl),
|
||||
Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Jxl,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frames: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"mp4",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Video(InternalVideoFormat::Mp4),
|
||||
width: 414,
|
||||
height: 261,
|
||||
frames: Some(17),
|
||||
},
|
||||
),
|
||||
(
|
||||
"png",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Image(ImageFormat::Png),
|
||||
Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Png,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 497,
|
||||
height: 694,
|
||||
frames: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"webm",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Video(InternalVideoFormat::Webm),
|
||||
width: 112,
|
||||
height: 112,
|
||||
frames: Some(27),
|
||||
},
|
||||
),
|
||||
(
|
||||
"webp",
|
||||
DiscoveryLite {
|
||||
format: InternalFormat::Image(ImageFormat::Webp),
|
||||
Discovery {
|
||||
input: InputFile::Image(ImageInput {
|
||||
format: ImageFormat::Webp,
|
||||
needs_reorient: false,
|
||||
}),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
frames: None,
|
||||
|
@ -98,7 +95,7 @@ fn parse_discovery() {
|
|||
|
||||
let json: Vec<MagickDiscovery> = serde_json::from_str(&string).expect("Valid json");
|
||||
|
||||
let output = super::parse_discovery(json).expect("Parsed details").lite();
|
||||
let output = super::parse_discovery(json).expect("Parsed details");
|
||||
|
||||
assert_eq!(output, expected);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ use std::str::FromStr;
|
|||
pub(crate) use animation::{AnimationFormat, AnimationOutput};
|
||||
pub(crate) use image::{ImageFormat, ImageInput, ImageOutput};
|
||||
pub(crate) use video::{
|
||||
AudioCodec, InternalVideoFormat, OutputVideoFormat, VideoCodec, VideoFormat,
|
||||
AlphaCodec, AudioCodec, InputVideoFormat, InternalVideoFormat, Mp4AudioCodec, Mp4Codec,
|
||||
OutputVideo, VideoCodec, WebmAlphaCodec, WebmAudioCodec, WebmCodec,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -22,7 +23,7 @@ pub(crate) struct Validations<'a> {
|
|||
pub(crate) enum InputFile {
|
||||
Image(ImageInput),
|
||||
Animation(AnimationFormat),
|
||||
Video(VideoFormat),
|
||||
Video(InputVideoFormat),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum InputVideoFormat {
|
||||
Mp4 {
|
||||
video_codec: Mp4Codec,
|
||||
audio_codec: Option<Mp4AudioCodec>,
|
||||
},
|
||||
Webm {
|
||||
video_codec: WebmCodec,
|
||||
audio_codec: Option<WebmAudioCodec>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub(crate) enum VideoFormat {
|
||||
Mp4,
|
||||
Webm { alpha: bool },
|
||||
pub(crate) struct OutputVideo {
|
||||
pub(crate) transcode_video: bool,
|
||||
pub(crate) transcode_audio: bool,
|
||||
pub(crate) format: OutputVideoFormat,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
@ -70,6 +83,8 @@ pub(crate) enum AudioCodec {
|
|||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||
)]
|
||||
pub(crate) enum Mp4Codec {
|
||||
#[serde(rename = "av1")]
|
||||
Av1,
|
||||
#[serde(rename = "h264")]
|
||||
H264,
|
||||
#[serde(rename = "h265")]
|
||||
|
@ -125,113 +140,262 @@ pub(crate) enum InternalVideoFormat {
|
|||
Webm,
|
||||
}
|
||||
|
||||
impl VideoFormat {
|
||||
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||
const fn webm_audio(
|
||||
allow_audio: bool,
|
||||
has_audio: bool,
|
||||
prescribed: Option<AudioCodec>,
|
||||
provided: Option<WebmAudioCodec>,
|
||||
) -> (Option<WebmAudioCodec>, bool) {
|
||||
if allow_audio && has_audio {
|
||||
match prescribed {
|
||||
Some(AudioCodec::Opus) => (
|
||||
Some(WebmAudioCodec::Opus),
|
||||
!matches!(provided, Some(WebmAudioCodec::Opus)),
|
||||
),
|
||||
Some(AudioCodec::Vorbis) => (
|
||||
Some(WebmAudioCodec::Vorbis),
|
||||
!matches!(provided, Some(WebmAudioCodec::Vorbis)),
|
||||
),
|
||||
_ => (provided, false),
|
||||
}
|
||||
} else {
|
||||
(None, false)
|
||||
}
|
||||
}
|
||||
|
||||
const fn mp4_audio(
|
||||
allow_audio: bool,
|
||||
has_audio: bool,
|
||||
prescribed: Option<AudioCodec>,
|
||||
provided: Option<Mp4AudioCodec>,
|
||||
) -> (Option<Mp4AudioCodec>, bool) {
|
||||
if allow_audio && has_audio {
|
||||
match prescribed {
|
||||
Some(AudioCodec::Aac) => (
|
||||
Some(Mp4AudioCodec::Aac),
|
||||
!matches!(provided, Some(Mp4AudioCodec::Aac)),
|
||||
),
|
||||
_ => (provided, false),
|
||||
}
|
||||
} else {
|
||||
(None, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl InputVideoFormat {
|
||||
pub(crate) const fn internal_format(self) -> InternalVideoFormat {
|
||||
match self {
|
||||
Self::Mp4 => "mp4",
|
||||
Self::Webm { .. } => "webm",
|
||||
Self::Mp4 { .. } => InternalVideoFormat::Mp4,
|
||||
Self::Webm { .. } => InternalVideoFormat::Webm,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn internal_format(self) -> InternalVideoFormat {
|
||||
const fn transcode_vorbis(
|
||||
self,
|
||||
prescribed_codec: WebmAlphaCodec,
|
||||
prescribed_audio_codec: Option<AudioCodec>,
|
||||
allow_audio: bool,
|
||||
) -> OutputVideo {
|
||||
match self {
|
||||
Self::Mp4 => InternalVideoFormat::Mp4,
|
||||
Self::Webm { .. } => InternalVideoFormat::Webm,
|
||||
Self::Webm {
|
||||
video_codec,
|
||||
audio_codec,
|
||||
} => {
|
||||
let (audio_codec, transcode_audio) = webm_audio(
|
||||
allow_audio,
|
||||
audio_codec.is_some(),
|
||||
prescribed_audio_codec,
|
||||
audio_codec,
|
||||
);
|
||||
|
||||
let (alpha, transcode_video) = match video_codec {
|
||||
WebmCodec::Alpha(AlphaCodec { alpha, codec }) => {
|
||||
(alpha, !codec.const_eq(prescribed_codec))
|
||||
}
|
||||
WebmCodec::Av1 => (false, true),
|
||||
};
|
||||
|
||||
OutputVideo {
|
||||
format: OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha,
|
||||
codec: prescribed_codec,
|
||||
}),
|
||||
audio_codec,
|
||||
},
|
||||
transcode_video,
|
||||
transcode_audio,
|
||||
}
|
||||
}
|
||||
Self::Mp4 { audio_codec, .. } => {
|
||||
let (audio_codec, transcode_audio) = webm_audio(
|
||||
allow_audio,
|
||||
audio_codec.is_some(),
|
||||
prescribed_audio_codec,
|
||||
None,
|
||||
);
|
||||
|
||||
OutputVideo {
|
||||
format: OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: prescribed_codec,
|
||||
}),
|
||||
audio_codec,
|
||||
},
|
||||
transcode_video: true,
|
||||
transcode_audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn transcode_av1(
|
||||
self,
|
||||
prescribed_audio_codec: Option<AudioCodec>,
|
||||
allow_audio: bool,
|
||||
) -> OutputVideo {
|
||||
match self {
|
||||
Self::Webm {
|
||||
video_codec,
|
||||
audio_codec,
|
||||
} => {
|
||||
let (audio_codec, transcode_audio) = webm_audio(
|
||||
allow_audio,
|
||||
audio_codec.is_some(),
|
||||
prescribed_audio_codec,
|
||||
audio_codec,
|
||||
);
|
||||
|
||||
OutputVideo {
|
||||
format: OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec,
|
||||
},
|
||||
transcode_video: !video_codec.const_eq(WebmCodec::Av1),
|
||||
transcode_audio,
|
||||
}
|
||||
}
|
||||
Self::Mp4 { audio_codec, .. } => {
|
||||
let (audio_codec, transcode_audio) = webm_audio(
|
||||
allow_audio,
|
||||
audio_codec.is_some(),
|
||||
prescribed_audio_codec,
|
||||
None,
|
||||
);
|
||||
|
||||
OutputVideo {
|
||||
format: OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec,
|
||||
},
|
||||
transcode_video: true,
|
||||
transcode_audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn transcode_mp4(
|
||||
self,
|
||||
prescribed_codec: Mp4Codec,
|
||||
prescribed_audio_codec: Option<AudioCodec>,
|
||||
allow_audio: bool,
|
||||
) -> OutputVideo {
|
||||
match self {
|
||||
Self::Mp4 {
|
||||
video_codec,
|
||||
audio_codec,
|
||||
} => {
|
||||
let (audio_codec, transcode_audio) = mp4_audio(
|
||||
allow_audio,
|
||||
audio_codec.is_some(),
|
||||
prescribed_audio_codec,
|
||||
audio_codec,
|
||||
);
|
||||
|
||||
OutputVideo {
|
||||
format: OutputVideoFormat::Mp4 {
|
||||
video_codec: prescribed_codec,
|
||||
audio_codec,
|
||||
},
|
||||
transcode_video: !video_codec.const_eq(prescribed_codec),
|
||||
transcode_audio,
|
||||
}
|
||||
}
|
||||
Self::Webm { audio_codec, .. } => {
|
||||
let (audio_codec, transcode_audio) = mp4_audio(
|
||||
allow_audio,
|
||||
audio_codec.is_some(),
|
||||
prescribed_audio_codec,
|
||||
None,
|
||||
);
|
||||
|
||||
OutputVideo {
|
||||
format: OutputVideoFormat::Mp4 {
|
||||
video_codec: prescribed_codec,
|
||||
audio_codec,
|
||||
},
|
||||
transcode_video: true,
|
||||
transcode_audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn build_output(
|
||||
self,
|
||||
video_codec: VideoCodec,
|
||||
audio_codec: Option<AudioCodec>,
|
||||
prescribed_video_codec: Option<VideoCodec>,
|
||||
prescribed_audio_codec: Option<AudioCodec>,
|
||||
allow_audio: bool,
|
||||
) -> OutputVideoFormat {
|
||||
match (video_codec, self) {
|
||||
(VideoCodec::Vp8, Self::Webm { alpha }) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha,
|
||||
codec: WebmAlphaCodec::Vp8,
|
||||
}),
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
) -> OutputVideo {
|
||||
match prescribed_video_codec {
|
||||
Some(VideoCodec::Vp8) => {
|
||||
self.transcode_vorbis(WebmAlphaCodec::Vp8, prescribed_audio_codec, allow_audio)
|
||||
}
|
||||
Some(VideoCodec::Vp9) => {
|
||||
self.transcode_vorbis(WebmAlphaCodec::Vp9, prescribed_audio_codec, allow_audio)
|
||||
}
|
||||
Some(VideoCodec::Av1) => self.transcode_av1(prescribed_audio_codec, allow_audio),
|
||||
Some(VideoCodec::H264) => {
|
||||
self.transcode_mp4(Mp4Codec::H264, prescribed_audio_codec, allow_audio)
|
||||
}
|
||||
Some(VideoCodec::H265) => {
|
||||
self.transcode_mp4(Mp4Codec::H265, prescribed_audio_codec, allow_audio)
|
||||
}
|
||||
None => OutputVideo {
|
||||
format: self.to_output(),
|
||||
transcode_video: false,
|
||||
transcode_audio: false,
|
||||
},
|
||||
(VideoCodec::Vp8, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp8,
|
||||
}),
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fn to_output(self) -> OutputVideoFormat {
|
||||
match self {
|
||||
Self::Mp4 {
|
||||
video_codec,
|
||||
audio_codec,
|
||||
} => OutputVideoFormat::Mp4 {
|
||||
video_codec,
|
||||
audio_codec,
|
||||
},
|
||||
(VideoCodec::Vp9, Self::Webm { alpha }) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
(VideoCodec::Vp9, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
(VideoCodec::Av1, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
(VideoCodec::H264, _) => OutputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H264,
|
||||
audio_codec: if allow_audio {
|
||||
Some(Mp4AudioCodec::Aac)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
(VideoCodec::H265, _) => OutputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H265,
|
||||
audio_codec: if allow_audio {
|
||||
Some(Mp4AudioCodec::Aac)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
Self::Webm {
|
||||
video_codec,
|
||||
audio_codec,
|
||||
} => OutputVideoFormat::Webm {
|
||||
video_codec,
|
||||
audio_codec,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||
match self {
|
||||
Self::Mp4 { .. } => "mp4",
|
||||
Self::Webm { .. } => "webm",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputVideoFormat {
|
||||
|
@ -242,92 +406,6 @@ impl OutputVideoFormat {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn from_parts(
|
||||
video_codec: VideoCodec,
|
||||
audio_codec: Option<AudioCodec>,
|
||||
allow_audio: bool,
|
||||
) -> Self {
|
||||
match (video_codec, audio_codec) {
|
||||
(VideoCodec::Av1, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
||||
},
|
||||
(VideoCodec::Av1, _) if allow_audio => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec: Some(WebmAudioCodec::Opus),
|
||||
},
|
||||
(VideoCodec::Av1, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec: None,
|
||||
},
|
||||
(VideoCodec::H264, _) if allow_audio => OutputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H264,
|
||||
audio_codec: Some(Mp4AudioCodec::Aac),
|
||||
},
|
||||
(VideoCodec::H264, _) => OutputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H264,
|
||||
audio_codec: None,
|
||||
},
|
||||
(VideoCodec::H265, _) if allow_audio => OutputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H265,
|
||||
audio_codec: Some(Mp4AudioCodec::Aac),
|
||||
},
|
||||
(VideoCodec::H265, _) => OutputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H265,
|
||||
audio_codec: None,
|
||||
},
|
||||
(VideoCodec::Vp8, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp8,
|
||||
}),
|
||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
||||
},
|
||||
(VideoCodec::Vp8, _) if allow_audio => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp8,
|
||||
}),
|
||||
audio_codec: Some(WebmAudioCodec::Opus),
|
||||
},
|
||||
(VideoCodec::Vp8, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp8,
|
||||
}),
|
||||
audio_codec: None,
|
||||
},
|
||||
(VideoCodec::Vp9, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
||||
},
|
||||
(VideoCodec::Vp9, _) if allow_audio => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: Some(WebmAudioCodec::Opus),
|
||||
},
|
||||
(VideoCodec::Vp9, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn magick_format(self) -> &'static str {
|
||||
match self {
|
||||
Self::Mp4 { .. } => "MP4",
|
||||
Self::Webm { .. } => "WEBM",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||
match self {
|
||||
Self::Mp4 { .. } => "mp4",
|
||||
|
@ -372,14 +450,28 @@ impl OutputVideoFormat {
|
|||
}
|
||||
|
||||
impl Mp4Codec {
|
||||
const fn const_eq(self, rhs: Self) -> bool {
|
||||
match (self, rhs) {
|
||||
(Self::Av1, Self::Av1) | (Self::H264, Self::H264) | (Self::H265, Self::H265) => true,
|
||||
(Self::Av1, _) | (Self::H264, _) | (Self::H265, _) => false,
|
||||
}
|
||||
}
|
||||
|
||||
const fn ffmpeg_codec(self) -> &'static str {
|
||||
match self {
|
||||
Self::Av1 => "av1",
|
||||
Self::H264 => "h264",
|
||||
Self::H265 => "hevc",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AlphaCodec {
|
||||
const fn const_eq(self, rhs: Self) -> bool {
|
||||
self.alpha == rhs.alpha && self.codec.const_eq(rhs.codec)
|
||||
}
|
||||
}
|
||||
|
||||
impl WebmAlphaCodec {
|
||||
const fn is_vp9(&self) -> bool {
|
||||
matches!(self, Self::Vp9)
|
||||
|
@ -391,9 +483,24 @@ impl WebmAlphaCodec {
|
|||
Self::Vp9 => "vp9",
|
||||
}
|
||||
}
|
||||
|
||||
const fn const_eq(self, rhs: Self) -> bool {
|
||||
match (self, rhs) {
|
||||
(Self::Vp8, Self::Vp8) | (Self::Vp9, Self::Vp9) => true,
|
||||
(Self::Vp8, _) | (Self::Vp9, _) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebmCodec {
|
||||
const fn const_eq(self, rhs: Self) -> bool {
|
||||
match (self, rhs) {
|
||||
(Self::Av1, Self::Av1) => true,
|
||||
(Self::Alpha(this), Self::Alpha(rhs)) => this.const_eq(rhs),
|
||||
(Self::Av1, _) | (Self::Alpha(_), _) => false,
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_vp9(self) -> bool {
|
||||
match self {
|
||||
Self::Av1 => false,
|
||||
|
|
|
@ -7,8 +7,8 @@ use crate::{
|
|||
either::Either,
|
||||
error::Error,
|
||||
formats::{
|
||||
AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InternalFormat,
|
||||
OutputVideoFormat, Validations, VideoFormat,
|
||||
AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InputVideoFormat,
|
||||
InternalFormat, Validations,
|
||||
},
|
||||
};
|
||||
use actix_web::web::Bytes;
|
||||
|
@ -71,7 +71,7 @@ pub(crate) async fn validate_bytes(
|
|||
width,
|
||||
height,
|
||||
frames.unwrap_or(1),
|
||||
&validations,
|
||||
validations.animation,
|
||||
timeout,
|
||||
)
|
||||
.await?;
|
||||
|
@ -81,7 +81,7 @@ pub(crate) async fn validate_bytes(
|
|||
InputFile::Video(input) => {
|
||||
let (format, read) = process_video(
|
||||
bytes,
|
||||
*input,
|
||||
input.clone(),
|
||||
width,
|
||||
height,
|
||||
frames.unwrap_or(1),
|
||||
|
@ -166,47 +166,25 @@ async fn process_animation(
|
|||
width: u16,
|
||||
height: u16,
|
||||
frames: u32,
|
||||
validations: &Validations<'_>,
|
||||
validations: &crate::config::Animation,
|
||||
timeout: u64,
|
||||
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
|
||||
match validate_animation(bytes.len(), width, height, frames, validations.animation) {
|
||||
Ok(()) => {
|
||||
let AnimationOutput {
|
||||
format,
|
||||
needs_transcode,
|
||||
} = input.build_output(validations.animation.format);
|
||||
validate_animation(bytes.len(), width, height, frames, validations)?;
|
||||
|
||||
let read = if needs_transcode {
|
||||
let quality = validations.animation.quality_for(format);
|
||||
let AnimationOutput {
|
||||
format,
|
||||
needs_transcode,
|
||||
} = input.build_output(validations.format);
|
||||
|
||||
Either::left(
|
||||
magick::convert_animation(input, format, quality, timeout, bytes).await?,
|
||||
)
|
||||
} else {
|
||||
Either::right(Either::left(exiftool::clear_metadata_bytes_read(
|
||||
bytes, timeout,
|
||||
)?))
|
||||
};
|
||||
let read = if needs_transcode {
|
||||
let quality = validations.quality_for(format);
|
||||
|
||||
Ok((InternalFormat::Animation(format), read))
|
||||
}
|
||||
Err(_) => match validate_video(bytes.len(), width, height, frames, validations.video) {
|
||||
Ok(()) => {
|
||||
let output = OutputVideoFormat::from_parts(
|
||||
validations.video.video_codec,
|
||||
validations.video.audio_codec,
|
||||
validations.video.allow_audio,
|
||||
);
|
||||
Either::left(magick::convert_animation(input, format, quality, timeout, bytes).await?)
|
||||
} else {
|
||||
Either::right(exiftool::clear_metadata_bytes_read(bytes, timeout)?)
|
||||
};
|
||||
|
||||
let read = Either::right(Either::right(
|
||||
magick::convert_video(input, output, timeout, bytes).await?,
|
||||
));
|
||||
|
||||
Ok((InternalFormat::Video(output.internal_format()), read))
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
},
|
||||
}
|
||||
Ok((InternalFormat::Animation(format), read))
|
||||
}
|
||||
|
||||
fn validate_video(
|
||||
|
@ -241,7 +219,7 @@ fn validate_video(
|
|||
#[tracing::instrument(skip(bytes, validations))]
|
||||
async fn process_video(
|
||||
bytes: Bytes,
|
||||
input: VideoFormat,
|
||||
input: InputVideoFormat,
|
||||
width: u16,
|
||||
height: u16,
|
||||
frames: u32,
|
||||
|
@ -260,5 +238,5 @@ async fn process_video(
|
|||
|
||||
let read = ffmpeg::transcode_bytes(input, output, crf, timeout, bytes).await?;
|
||||
|
||||
Ok((InternalFormat::Video(output.internal_format()), read))
|
||||
Ok((InternalFormat::Video(output.format.internal_format()), read))
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ use tokio::io::AsyncRead;
|
|||
|
||||
use crate::{
|
||||
ffmpeg::FfMpegError,
|
||||
formats::{OutputVideoFormat, VideoFormat},
|
||||
formats::{InputVideoFormat, OutputVideo},
|
||||
process::Process,
|
||||
};
|
||||
|
||||
pub(super) async fn transcode_bytes(
|
||||
input_format: VideoFormat,
|
||||
output_format: OutputVideoFormat,
|
||||
input_format: InputVideoFormat,
|
||||
output_format: OutputVideo,
|
||||
crf: u8,
|
||||
timeout: u64,
|
||||
bytes: Bytes,
|
||||
|
@ -57,12 +57,20 @@ pub(super) async fn transcode_bytes(
|
|||
|
||||
async fn transcode_files(
|
||||
input_path: &str,
|
||||
input_format: VideoFormat,
|
||||
input_format: InputVideoFormat,
|
||||
output_path: &str,
|
||||
output_format: OutputVideoFormat,
|
||||
output_format: OutputVideo,
|
||||
crf: u8,
|
||||
timeout: u64,
|
||||
) -> Result<(), FfMpegError> {
|
||||
let crf = crf.to_string();
|
||||
|
||||
let OutputVideo {
|
||||
transcode_video,
|
||||
transcode_audio,
|
||||
format: output_format,
|
||||
} = output_format;
|
||||
|
||||
let mut args = vec![
|
||||
"-hide_banner",
|
||||
"-v",
|
||||
|
@ -71,33 +79,38 @@ async fn transcode_files(
|
|||
input_format.ffmpeg_format(),
|
||||
"-i",
|
||||
input_path,
|
||||
"-pix_fmt",
|
||||
output_format.pix_fmt(),
|
||||
"-vf",
|
||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||
];
|
||||
|
||||
if let Some(audio_codec) = output_format.ffmpeg_audio_codec() {
|
||||
args.extend(["-c:a", audio_codec]);
|
||||
if transcode_video {
|
||||
args.extend([
|
||||
"-pix_fmt",
|
||||
output_format.pix_fmt(),
|
||||
"-vf",
|
||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||
"-c:v",
|
||||
output_format.ffmpeg_video_codec(),
|
||||
"-crf",
|
||||
&crf,
|
||||
]);
|
||||
|
||||
if output_format.is_vp9() {
|
||||
args.extend(["-b:v", "0"]);
|
||||
}
|
||||
} else {
|
||||
args.push("-an")
|
||||
args.extend(["-c:v", "copy"]);
|
||||
}
|
||||
|
||||
args.extend(["-c:v", output_format.ffmpeg_video_codec()]);
|
||||
|
||||
if output_format.is_vp9() {
|
||||
args.extend(["-b:v", "0"]);
|
||||
if transcode_audio {
|
||||
if let Some(audio_codec) = output_format.ffmpeg_audio_codec() {
|
||||
args.extend(["-c:a", audio_codec]);
|
||||
} else {
|
||||
args.push("-an")
|
||||
}
|
||||
} else {
|
||||
args.extend(["-c:a", "copy"]);
|
||||
}
|
||||
|
||||
let crf = crf.to_string();
|
||||
|
||||
args.extend([
|
||||
"-crf",
|
||||
&crf,
|
||||
"-f",
|
||||
output_format.ffmpeg_format(),
|
||||
output_path,
|
||||
]);
|
||||
args.extend(["-f", output_format.ffmpeg_format(), output_path]);
|
||||
|
||||
Process::run("ffmpeg", &args, timeout)?.wait().await?;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use actix_web::web::Bytes;
|
|||
use tokio::io::AsyncRead;
|
||||
|
||||
use crate::{
|
||||
formats::{AnimationFormat, ImageFormat, OutputVideoFormat},
|
||||
formats::{AnimationFormat, ImageFormat},
|
||||
magick::MagickError,
|
||||
process::Process,
|
||||
};
|
||||
|
@ -43,23 +43,6 @@ pub(super) async fn convert_animation(
|
|||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn convert_video(
|
||||
input: AnimationFormat,
|
||||
output: OutputVideoFormat,
|
||||
timeout: u64,
|
||||
bytes: Bytes,
|
||||
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
||||
convert(
|
||||
input.magick_format(),
|
||||
output.magick_format(),
|
||||
true,
|
||||
None,
|
||||
timeout,
|
||||
bytes,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn convert(
|
||||
input: &'static str,
|
||||
output: &'static str,
|
||||
|
|
Loading…
Reference in a new issue