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:
asonix 2023-08-30 20:37:54 -05:00
parent 08fd96c2f7
commit b48a9233b2
26 changed files with 858 additions and 716 deletions

View File

@ -1,6 +1,5 @@
use crate::{ use crate::{
config::primitives::{LogFormat, Targets}, config::primitives::{LogFormat, Targets},
formats::VideoCodec,
serde_str::Serde, serde_str::Serde,
}; };
use std::{net::SocketAddr, path::PathBuf}; use std::{net::SocketAddr, path::PathBuf};
@ -120,7 +119,6 @@ struct VideoDefaults {
max_area: u32, max_area: u32,
max_frame_count: u32, max_frame_count: u32,
max_file_size: usize, max_file_size: usize,
video_codec: VideoCodec,
quality: VideoQualityDefaults, quality: VideoQualityDefaults,
} }
@ -283,7 +281,6 @@ impl Default for VideoDefaults {
max_area: 8_294_400, max_area: 8_294_400,
max_frame_count: 900, max_frame_count: 900,
max_file_size: 40, max_file_size: 40,
video_codec: VideoCodec::Vp9,
quality: VideoQualityDefaults::default(), quality: VideoQualityDefaults::default(),
} }
} }

View File

@ -300,7 +300,8 @@ pub(crate) struct Video {
pub(crate) max_frame_count: u32, 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, pub(crate) quality: VideoQuality,

View File

@ -1,9 +1,11 @@
use crate::{ use crate::{
discover::DiscoveryLite, bytes_stream::BytesStream,
discover::Discovery,
error::Error, error::Error,
formats::{InternalFormat, InternalVideoFormat}, formats::{InternalFormat, InternalVideoFormat},
serde_str::Serde, serde_str::Serde,
store::Store, store::Store,
stream::IntoStreamer,
}; };
use actix_web::web; use actix_web::web;
use time::{format_description::well_known::Rfc3339, OffsetDateTime}; 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> { pub(crate) async fn from_bytes(timeout: u64, input: web::Bytes) -> Result<Self, Error> {
let DiscoveryLite { let Discovery {
format, input,
width, width,
height, height,
frames, 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>( pub(crate) async fn from_store<S: Store>(
@ -50,14 +57,20 @@ impl Details {
identifier: &S::Identifier, identifier: &S::Identifier,
timeout: u64, timeout: u64,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let DiscoveryLite { let mut buf = BytesStream::new();
format,
width,
height,
frames,
} = crate::discover::discover_store_lite(store, identifier, timeout).await?;
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 { pub(crate) fn internal_format(&self) -> InternalFormat {

View File

@ -4,10 +4,7 @@ mod magick;
use actix_web::web::Bytes; use actix_web::web::Bytes;
use crate::{ use crate::formats::InputFile;
formats::{InputFile, InternalFormat},
store::Store,
};
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub(crate) struct Discovery { pub(crate) struct Discovery {
@ -17,14 +14,6 @@ pub(crate) struct Discovery {
pub(crate) frames: Option<u32>, 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)] #[derive(Debug, thiserror::Error)]
pub(crate) enum DiscoverError { pub(crate) enum DiscoverError {
#[error("No frames in uploaded media")] #[error("No frames in uploaded media")]
@ -37,41 +26,6 @@ pub(crate) enum DiscoverError {
UnsupportedFileType(String), 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( pub(crate) async fn discover_bytes(
timeout: u64, timeout: u64,
bytes: Bytes, bytes: Bytes,

View File

@ -6,40 +6,134 @@ use std::{collections::HashSet, sync::OnceLock};
use crate::{ use crate::{
ffmpeg::FfMpegError, ffmpeg::FfMpegError,
formats::{ formats::{
AnimationFormat, ImageFormat, ImageInput, InputFile, InternalFormat, InternalVideoFormat, AlphaCodec, AnimationFormat, ImageFormat, ImageInput, InputFile, InputVideoFormat,
VideoFormat, Mp4AudioCodec, Mp4Codec, WebmAlphaCodec, WebmAudioCodec, WebmCodec,
}, },
process::Process, process::Process,
}; };
use actix_web::web::Bytes; use actix_web::web::Bytes;
use futures_core::Stream;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use super::{Discovery, DiscoveryLite}; use super::Discovery;
const MP4: &str = "mp4"; 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)] #[derive(Debug, serde::Deserialize)]
struct FfMpegDiscovery { struct FfMpegDiscovery {
streams: [FfMpegStream; 1], streams: FfMpegStreams,
format: FfMpegFormat, format: FfMpegFormat,
} }
#[derive(Debug, serde::Deserialize)] #[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, width: u16,
height: 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)] #[derive(Debug, serde::Deserialize)]
@ -67,7 +161,7 @@ pub(super) async fn discover_bytes(
timeout: u64, timeout: u64,
bytes: Bytes, bytes: Bytes,
) -> Result<Option<Discovery>, FfMpegError> { ) -> Result<Option<Discovery>, FfMpegError> {
discover_file_full( discover_file(
move |mut file| { move |mut file| {
let bytes = bytes.clone(); let bytes = bytes.clone();
@ -83,130 +177,22 @@ pub(super) async fn discover_bytes(
.await .await
} }
pub(super) async fn discover_bytes_lite( async fn allows_alpha(pixel_format: &str, timeout: u64) -> Result<bool, FfMpegError> {
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
}
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(); static ALPHA_PIXEL_FORMATS: OnceLock<HashSet<String>> = OnceLock::new();
let format = pixel_format(f, timeout).await?; match ALPHA_PIXEL_FORMATS.get() {
Some(alpha_pixel_formats) => Ok(alpha_pixel_formats.contains(pixel_format)),
let alpha = match ALPHA_PIXEL_FORMATS.get() {
Some(alpha_pixel_formats) => alpha_pixel_formats.contains(&format),
None => { None => {
let pixel_formats = alpha_pixel_formats(timeout).await?; let pixel_formats = alpha_pixel_formats(timeout).await?;
let alpha = pixel_formats.contains(&format); let alpha = pixel_formats.contains(pixel_format);
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats); let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
alpha Ok(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(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))] #[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 where
F: FnOnce(crate::file::File) -> Fut, F: FnOnce(crate::file::File) -> Fut,
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>, Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
@ -228,11 +214,9 @@ where
&[ &[
"-v", "-v",
"quiet", "quiet",
"-select_streams",
"v:0",
"-count_frames", "-count_frames",
"-show_entries", "-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", "-of",
"default=noprint_wrappers=1:nokey=1", "default=noprint_wrappers=1:nokey=1",
"-print_format", "-print_format",
@ -254,54 +238,23 @@ where
let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?; let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
parse_discovery(output) let (discovery, pix_fmt) = parse_discovery(output)?;
let Some(mut discovery) = discovery else {
return Ok(None);
};
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?;
}
} }
async fn pixel_format<F, Fut>(f: F, timeout: u64) -> Result<String, FfMpegError> Ok(Some(discovery))
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",
"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())
} }
async fn alpha_pixel_formats(timeout: u64) -> Result<HashSet<String>, FfMpegError> { async fn alpha_pixel_formats(timeout: u64) -> Result<HashSet<String>, FfMpegError> {
@ -346,56 +299,145 @@ fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet<String> {
.collect() .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 { let FfMpegDiscovery {
streams: streams,
[FfMpegStream {
width,
height,
nb_read_frames,
}],
format: FfMpegFormat { format_name }, format: FfMpegFormat { format_name },
} = discovery; } = discovery;
if let Some((name, value)) = FFMPEG_FORMAT_MAPPINGS let Some((video_stream, audio_stream)) = streams.into_parts() else {
.iter() tracing::info!("No matching format mapping for {format_name}");
.find(|(name, _)| format_name.contains(name)) return Ok((None, None));
{ };
let frames = nb_read_frames.and_then(|frames| frames.parse().ok());
if *name == MP4 && frames.map(|nb| nb == 1).unwrap_or(false) { let input = match video_stream.codec_name {
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed mp4 even when 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 // animated
return Ok(Some(DiscoveryLite { return Ok((
format: InternalFormat::Animation(AnimationFormat::Avif), Some(Discovery {
width, input: InputFile::Animation(AnimationFormat::Avif),
height, width: video_stream.width,
height: video_stream.height,
frames: None, frames: None,
})); }),
None,
));
} }
FfMpegVideoCodec::Webp
if *name == WEBP && (frames.is_none() || width == 0 || height == 0) { 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 // Might be Animated Webp, ffmpeg incorrectly detects animated webp as having no frames
// and 0 dimensions // and 0 dimensions
return Ok(Some(DiscoveryLite { return Ok((
format: InternalFormat::Animation(AnimationFormat::Webp), Some(Discovery {
width, input: InputFile::Animation(AnimationFormat::Webp),
height, width: video_stream.width,
frames, 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 { Ok((
format: *value, Some(Discovery {
width, input,
height, width: video_stream.width,
frames: frames.and_then(|frames| if frames > 1 { Some(frames) } else { None }), height: video_stream.height,
})); frames: video_stream.nb_read_frames.and_then(|f| {
if f.frames <= 1 {
None
} else {
Some(f.frames)
} }
}),
tracing::info!("No matching format mapping for {format_name}"); }),
video_stream.pix_fmt,
Ok(None) ))
} }

View 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"
}
}

View File

@ -4,6 +4,7 @@
], ],
"streams": [ "streams": [
{ {
"codec_name": "webp",
"width": 0, "width": 0,
"height": 0 "height": 0
} }

View File

@ -4,8 +4,10 @@
], ],
"streams": [ "streams": [
{ {
"codec_name": "apng",
"width": 112, "width": 112,
"height": 112, "height": 112,
"pix_fmt": "rgba",
"nb_read_frames": "27" "nb_read_frames": "27"
} }
], ],

View File

@ -4,8 +4,10 @@
], ],
"streams": [ "streams": [
{ {
"width": 1920, "codec_name": "av1",
"height": 1080, "width": 1200,
"height": 1387,
"pix_fmt": "yuv420p",
"nb_read_frames": "1" "nb_read_frames": "1"
} }
], ],

View File

@ -4,9 +4,11 @@
], ],
"streams": [ "streams": [
{ {
"width": 160, "codec_name": "gif",
"height": 227, "width": 112,
"nb_read_frames": "28" "height": 112,
"pix_fmt": "bgra",
"nb_read_frames": "27"
} }
], ],
"format": { "format": {

View File

@ -4,8 +4,10 @@
], ],
"streams": [ "streams": [
{ {
"width": 1920, "codec_name": "mjpeg",
"height": 1080, "width": 1663,
"height": 1247,
"pix_fmt": "yuvj420p",
"nb_read_frames": "1" "nb_read_frames": "1"
} }
], ],

View File

@ -4,6 +4,7 @@
], ],
"streams": [ "streams": [
{ {
"codec_name": "jpegxl",
"width": 0, "width": 0,
"height": 0 "height": 0
} }

View 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"
}
}

View File

@ -4,9 +4,11 @@
], ],
"streams": [ "streams": [
{ {
"width": 852, "codec_name": "h264",
"height": 480, "width": 1426,
"nb_read_frames": "35364" "height": 834,
"pix_fmt": "yuv420p",
"nb_read_frames": "105"
} }
], ],
"format": { "format": {

View File

@ -4,8 +4,10 @@
], ],
"streams": [ "streams": [
{ {
"codec_name": "png",
"width": 450, "width": 450,
"height": 401, "height": 401,
"pix_fmt": "rgb24",
"nb_read_frames": "1" "nb_read_frames": "1"
} }
], ],

View File

@ -4,8 +4,10 @@
], ],
"streams": [ "streams": [
{ {
"codec_name": "av1",
"width": 112, "width": 112,
"height": 112, "height": 112,
"pix_fmt": "gbrp",
"nb_read_frames": "27" "nb_read_frames": "27"
} }
], ],

View File

@ -4,9 +4,11 @@
], ],
"streams": [ "streams": [
{ {
"width": 640, "codec_name": "vp9",
"height": 480, "width": 112,
"nb_read_frames": "34650" "height": 112,
"pix_fmt": "yuv420p",
"nb_read_frames": "27"
} }
], ],
"format": { "format": {

View File

@ -4,8 +4,10 @@
], ],
"streams": [ "streams": [
{ {
"width": 1920, "codec_name": "webp",
"height": 1080, "width": 1200,
"height": 1387,
"pix_fmt": "yuv420p",
"nb_read_frames": "1" "nb_read_frames": "1"
} }
], ],

View File

@ -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", "animated_webp",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Animation(AnimationFormat::Webp), input: InputFile::Animation(AnimationFormat::Webp),
width: 0, width: 0,
height: 0, height: 0,
frames: None, frames: None,
}), }),
), ),
(
"animated_avif",
Some(Discovery {
input: InputFile::Animation(AnimationFormat::Avif),
width: 112,
height: 112,
frames: None,
}),
),
( (
"apng", "apng",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Animation(AnimationFormat::Apng), input: InputFile::Animation(AnimationFormat::Apng),
width: 112, width: 112,
height: 112, height: 112,
frames: Some(27), frames: Some(27),
@ -24,37 +36,77 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
), ),
( (
"avif", "avif",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Animation(AnimationFormat::Avif), input: InputFile::Animation(AnimationFormat::Avif),
width: 1920, width: 1200,
height: 1080, height: 1387,
frames: None, frames: None,
}), }),
), ),
( (
"gif", "gif",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Animation(AnimationFormat::Gif), input: InputFile::Animation(AnimationFormat::Gif),
width: 160, width: 112,
height: 227, height: 112,
frames: Some(28), 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", "mp4",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Video(InternalVideoFormat::Mp4), input: InputFile::Video(InputVideoFormat::Mp4 {
width: 852, video_codec: Mp4Codec::H264,
height: 480, audio_codec: None,
frames: Some(35364), }),
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", "png",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Image(ImageFormat::Png), input: InputFile::Image(ImageInput {
format: ImageFormat::Png,
needs_reorient: false,
}),
width: 450, width: 450,
height: 401, height: 401,
frames: None, frames: None,
@ -62,17 +114,26 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
), ),
( (
"webm", "webm",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Video(InternalVideoFormat::Webm), input: InputFile::Video(InputVideoFormat::Webm {
width: 640, video_codec: WebmCodec::Alpha(AlphaCodec {
height: 480, alpha: false,
frames: Some(34650), codec: WebmAlphaCodec::Vp9,
}),
audio_codec: None,
}),
width: 112,
height: 112,
frames: Some(27),
}), }),
), ),
( (
"webm_av1", "webm_av1",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Video(InternalVideoFormat::Webm), input: InputFile::Video(InputVideoFormat::Webm {
video_codec: WebmCodec::Av1,
audio_codec: None,
}),
width: 112, width: 112,
height: 112, height: 112,
frames: Some(27), frames: Some(27),
@ -80,10 +141,13 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
), ),
( (
"webp", "webp",
Some(DiscoveryLite { Some(Discovery {
format: InternalFormat::Image(ImageFormat::Webp), input: InputFile::Image(ImageInput {
width: 1920, format: ImageFormat::Webp,
height: 1080, needs_reorient: false,
}),
width: 1200,
height: 1387,
frames: None, frames: None,
}), }),
), ),
@ -100,7 +164,7 @@ fn parse_discovery() {
let json: FfMpegDiscovery = serde_json::from_str(&string).expect("Valid json"); 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); assert_eq!(output, expected);
} }

View File

@ -2,17 +2,16 @@
mod tests; mod tests;
use actix_web::web::Bytes; use actix_web::web::Bytes;
use futures_core::Stream;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use crate::{ use crate::{
discover::DiscoverError, discover::DiscoverError,
formats::{AnimationFormat, ImageFormat, ImageInput, InputFile, VideoFormat}, formats::{AnimationFormat, ImageFormat, ImageInput, InputFile},
magick::MagickError, magick::MagickError,
process::Process, process::Process,
}; };
use super::{Discovery, DiscoveryLite}; use super::Discovery;
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
struct MagickDiscovery { struct MagickDiscovery {
@ -31,59 +30,6 @@ struct Geometry {
height: u16, 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( pub(super) async fn confirm_bytes(
discovery: Option<Discovery>, discovery: Option<Discovery>,
timeout: u64, timeout: u64,
@ -107,6 +53,18 @@ pub(super) async fn confirm_bytes(
) )
.await?; .await?;
if frames == 1 {
return Ok(Discovery {
input: InputFile::Image(ImageInput {
format: ImageFormat::Avif,
needs_reorient: false,
}),
width,
height,
frames: None,
});
}
return Ok(Discovery { return Ok(Discovery {
input: InputFile::Animation(AnimationFormat::Avif), input: InputFile::Animation(AnimationFormat::Avif),
width, width,
@ -189,14 +147,6 @@ where
Ok(lines) 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> async fn discover_file<F, Fut>(f: F, timeout: u64) -> Result<Discovery, MagickError>
where where
F: FnOnce(crate::file::File) -> Fut, F: FnOnce(crate::file::File) -> Fut,
@ -338,12 +288,6 @@ fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, DiscoverEr
height, height,
frames: None, frames: None,
}), }),
"MP4" => Ok(Discovery {
input: InputFile::Video(VideoFormat::Mp4),
width,
height,
frames: Some(frames),
}),
"PNG" => Ok(Discovery { "PNG" => Ok(Discovery {
input: InputFile::Image(ImageInput { input: InputFile::Image(ImageInput {
format: ImageFormat::Png, 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))), otherwise => Err(DiscoverError::UnsupportedFileType(String::from(otherwise))),
} }
} }

View File

@ -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", "animated_webp",
DiscoveryLite { Discovery {
format: InternalFormat::Animation(AnimationFormat::Webp), input: InputFile::Animation(AnimationFormat::Webp),
width: 112, width: 112,
height: 112, height: 112,
frames: Some(27), frames: Some(27),
@ -15,8 +15,11 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
), ),
( (
"avif", "avif",
DiscoveryLite { Discovery {
format: InternalFormat::Image(ImageFormat::Avif), input: InputFile::Image(ImageInput {
format: ImageFormat::Avif,
needs_reorient: false,
}),
width: 1920, width: 1920,
height: 1080, height: 1080,
frames: None, frames: None,
@ -24,8 +27,8 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
), ),
( (
"gif", "gif",
DiscoveryLite { Discovery {
format: InternalFormat::Animation(AnimationFormat::Gif), input: InputFile::Animation(AnimationFormat::Gif),
width: 414, width: 414,
height: 261, height: 261,
frames: Some(17), frames: Some(17),
@ -33,8 +36,11 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
), ),
( (
"jpeg", "jpeg",
DiscoveryLite { Discovery {
format: InternalFormat::Image(ImageFormat::Jpeg), input: InputFile::Image(ImageInput {
format: ImageFormat::Jpeg,
needs_reorient: false,
}),
width: 1920, width: 1920,
height: 1080, height: 1080,
frames: None, frames: None,
@ -42,44 +48,35 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
), ),
( (
"jxl", "jxl",
DiscoveryLite { Discovery {
format: InternalFormat::Image(ImageFormat::Jxl), input: InputFile::Image(ImageInput {
format: ImageFormat::Jxl,
needs_reorient: false,
}),
width: 1920, width: 1920,
height: 1080, height: 1080,
frames: None, frames: None,
}, },
), ),
(
"mp4",
DiscoveryLite {
format: InternalFormat::Video(InternalVideoFormat::Mp4),
width: 414,
height: 261,
frames: Some(17),
},
),
( (
"png", "png",
DiscoveryLite { Discovery {
format: InternalFormat::Image(ImageFormat::Png), input: InputFile::Image(ImageInput {
format: ImageFormat::Png,
needs_reorient: false,
}),
width: 497, width: 497,
height: 694, height: 694,
frames: None, frames: None,
}, },
), ),
(
"webm",
DiscoveryLite {
format: InternalFormat::Video(InternalVideoFormat::Webm),
width: 112,
height: 112,
frames: Some(27),
},
),
( (
"webp", "webp",
DiscoveryLite { Discovery {
format: InternalFormat::Image(ImageFormat::Webp), input: InputFile::Image(ImageInput {
format: ImageFormat::Webp,
needs_reorient: false,
}),
width: 1920, width: 1920,
height: 1080, height: 1080,
frames: None, frames: None,
@ -98,7 +95,7 @@ fn parse_discovery() {
let json: Vec<MagickDiscovery> = serde_json::from_str(&string).expect("Valid json"); 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); assert_eq!(output, expected);
} }

View File

@ -8,7 +8,8 @@ use std::str::FromStr;
pub(crate) use animation::{AnimationFormat, AnimationOutput}; pub(crate) use animation::{AnimationFormat, AnimationOutput};
pub(crate) use image::{ImageFormat, ImageInput, ImageOutput}; pub(crate) use image::{ImageFormat, ImageInput, ImageOutput};
pub(crate) use video::{ pub(crate) use video::{
AudioCodec, InternalVideoFormat, OutputVideoFormat, VideoCodec, VideoFormat, AlphaCodec, AudioCodec, InputVideoFormat, InternalVideoFormat, Mp4AudioCodec, Mp4Codec,
OutputVideo, VideoCodec, WebmAlphaCodec, WebmAudioCodec, WebmCodec,
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -22,7 +23,7 @@ pub(crate) struct Validations<'a> {
pub(crate) enum InputFile { pub(crate) enum InputFile {
Image(ImageInput), Image(ImageInput),
Animation(AnimationFormat), Animation(AnimationFormat),
Video(VideoFormat), Video(InputVideoFormat),
} }
#[derive( #[derive(

View File

@ -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)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum VideoFormat { pub(crate) struct OutputVideo {
Mp4, pub(crate) transcode_video: bool,
Webm { alpha: bool }, pub(crate) transcode_audio: bool,
pub(crate) format: OutputVideoFormat,
} }
#[derive( #[derive(
@ -70,6 +83,8 @@ pub(crate) enum AudioCodec {
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
)] )]
pub(crate) enum Mp4Codec { pub(crate) enum Mp4Codec {
#[serde(rename = "av1")]
Av1,
#[serde(rename = "h264")] #[serde(rename = "h264")]
H264, H264,
#[serde(rename = "h265")] #[serde(rename = "h265")]
@ -125,113 +140,262 @@ pub(crate) enum InternalVideoFormat {
Webm, Webm,
} }
impl VideoFormat { const fn webm_audio(
pub(crate) const fn ffmpeg_format(self) -> &'static str { allow_audio: bool,
match self { has_audio: bool,
Self::Mp4 => "mp4", prescribed: Option<AudioCodec>,
Self::Webm { .. } => "webm", 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 { pub(crate) const fn internal_format(self) -> InternalVideoFormat {
match self { match self {
Self::Mp4 => InternalVideoFormat::Mp4, Self::Mp4 { .. } => InternalVideoFormat::Mp4,
Self::Webm { .. } => InternalVideoFormat::Webm, Self::Webm { .. } => InternalVideoFormat::Webm,
} }
} }
const fn transcode_vorbis(
self,
prescribed_codec: WebmAlphaCodec,
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,
);
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( pub(crate) const fn build_output(
self, self,
video_codec: VideoCodec, prescribed_video_codec: Option<VideoCodec>,
audio_codec: Option<AudioCodec>, prescribed_audio_codec: Option<AudioCodec>,
allow_audio: bool, allow_audio: bool,
) -> OutputVideoFormat { ) -> OutputVideo {
match (video_codec, self) { match prescribed_video_codec {
(VideoCodec::Vp8, Self::Webm { alpha }) => OutputVideoFormat::Webm { Some(VideoCodec::Vp8) => {
video_codec: WebmCodec::Alpha(AlphaCodec { self.transcode_vorbis(WebmAlphaCodec::Vp8, prescribed_audio_codec, allow_audio)
alpha,
codec: WebmAlphaCodec::Vp8,
}),
audio_codec: if allow_audio {
match audio_codec {
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
_ => Some(WebmAudioCodec::Opus),
} }
} else { Some(VideoCodec::Vp9) => {
None self.transcode_vorbis(WebmAlphaCodec::Vp9, prescribed_audio_codec, allow_audio)
},
},
(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 { Some(VideoCodec::Av1) => self.transcode_av1(prescribed_audio_codec, allow_audio),
None Some(VideoCodec::H264) => {
}, self.transcode_mp4(Mp4Codec::H264, prescribed_audio_codec, allow_audio)
},
(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 { Some(VideoCodec::H265) => {
None self.transcode_mp4(Mp4Codec::H265, prescribed_audio_codec, allow_audio)
},
},
(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 => OutputVideo {
None format: self.to_output(),
transcode_video: false,
transcode_audio: false,
}, },
},
(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
}, const fn to_output(self) -> OutputVideoFormat {
}, match self {
(VideoCodec::H264, _) => OutputVideoFormat::Mp4 { Self::Mp4 {
video_codec: Mp4Codec::H264, video_codec,
audio_codec: if allow_audio { audio_codec,
Some(Mp4AudioCodec::Aac) } => OutputVideoFormat::Mp4 {
} else { video_codec,
None audio_codec,
},
},
(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 { 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 { pub(crate) const fn ffmpeg_format(self) -> &'static str {
match self { match self {
Self::Mp4 { .. } => "mp4", Self::Mp4 { .. } => "mp4",
@ -372,14 +450,28 @@ impl OutputVideoFormat {
} }
impl Mp4Codec { 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 { const fn ffmpeg_codec(self) -> &'static str {
match self { match self {
Self::Av1 => "av1",
Self::H264 => "h264", Self::H264 => "h264",
Self::H265 => "hevc", 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 { impl WebmAlphaCodec {
const fn is_vp9(&self) -> bool { const fn is_vp9(&self) -> bool {
matches!(self, Self::Vp9) matches!(self, Self::Vp9)
@ -391,9 +483,24 @@ impl WebmAlphaCodec {
Self::Vp9 => "vp9", 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 { 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 { const fn is_vp9(self) -> bool {
match self { match self {
Self::Av1 => false, Self::Av1 => false,

View File

@ -7,8 +7,8 @@ use crate::{
either::Either, either::Either,
error::Error, error::Error,
formats::{ formats::{
AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InternalFormat, AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InputVideoFormat,
OutputVideoFormat, Validations, VideoFormat, InternalFormat, Validations,
}, },
}; };
use actix_web::web::Bytes; use actix_web::web::Bytes;
@ -71,7 +71,7 @@ pub(crate) async fn validate_bytes(
width, width,
height, height,
frames.unwrap_or(1), frames.unwrap_or(1),
&validations, validations.animation,
timeout, timeout,
) )
.await?; .await?;
@ -81,7 +81,7 @@ pub(crate) async fn validate_bytes(
InputFile::Video(input) => { InputFile::Video(input) => {
let (format, read) = process_video( let (format, read) = process_video(
bytes, bytes,
*input, input.clone(),
width, width,
height, height,
frames.unwrap_or(1), frames.unwrap_or(1),
@ -166,48 +166,26 @@ async fn process_animation(
width: u16, width: u16,
height: u16, height: u16,
frames: u32, frames: u32,
validations: &Validations<'_>, validations: &crate::config::Animation,
timeout: u64, timeout: u64,
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> { ) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
match validate_animation(bytes.len(), width, height, frames, validations.animation) { validate_animation(bytes.len(), width, height, frames, validations)?;
Ok(()) => {
let AnimationOutput { let AnimationOutput {
format, format,
needs_transcode, needs_transcode,
} = input.build_output(validations.animation.format); } = input.build_output(validations.format);
let read = if needs_transcode { let read = if needs_transcode {
let quality = validations.animation.quality_for(format); let quality = validations.quality_for(format);
Either::left( Either::left(magick::convert_animation(input, format, quality, timeout, bytes).await?)
magick::convert_animation(input, format, quality, timeout, bytes).await?,
)
} else { } else {
Either::right(Either::left(exiftool::clear_metadata_bytes_read( Either::right(exiftool::clear_metadata_bytes_read(bytes, timeout)?)
bytes, timeout,
)?))
}; };
Ok((InternalFormat::Animation(format), read)) 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,
);
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()),
},
}
}
fn validate_video( fn validate_video(
size: usize, size: usize,
@ -241,7 +219,7 @@ fn validate_video(
#[tracing::instrument(skip(bytes, validations))] #[tracing::instrument(skip(bytes, validations))]
async fn process_video( async fn process_video(
bytes: Bytes, bytes: Bytes,
input: VideoFormat, input: InputVideoFormat,
width: u16, width: u16,
height: u16, height: u16,
frames: u32, frames: u32,
@ -260,5 +238,5 @@ async fn process_video(
let read = ffmpeg::transcode_bytes(input, output, crf, timeout, bytes).await?; 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))
} }

View File

@ -3,13 +3,13 @@ use tokio::io::AsyncRead;
use crate::{ use crate::{
ffmpeg::FfMpegError, ffmpeg::FfMpegError,
formats::{OutputVideoFormat, VideoFormat}, formats::{InputVideoFormat, OutputVideo},
process::Process, process::Process,
}; };
pub(super) async fn transcode_bytes( pub(super) async fn transcode_bytes(
input_format: VideoFormat, input_format: InputVideoFormat,
output_format: OutputVideoFormat, output_format: OutputVideo,
crf: u8, crf: u8,
timeout: u64, timeout: u64,
bytes: Bytes, bytes: Bytes,
@ -57,12 +57,20 @@ pub(super) async fn transcode_bytes(
async fn transcode_files( async fn transcode_files(
input_path: &str, input_path: &str,
input_format: VideoFormat, input_format: InputVideoFormat,
output_path: &str, output_path: &str,
output_format: OutputVideoFormat, output_format: OutputVideo,
crf: u8, crf: u8,
timeout: u64, timeout: u64,
) -> Result<(), FfMpegError> { ) -> Result<(), FfMpegError> {
let crf = crf.to_string();
let OutputVideo {
transcode_video,
transcode_audio,
format: output_format,
} = output_format;
let mut args = vec![ let mut args = vec![
"-hide_banner", "-hide_banner",
"-v", "-v",
@ -71,33 +79,38 @@ async fn transcode_files(
input_format.ffmpeg_format(), input_format.ffmpeg_format(),
"-i", "-i",
input_path, input_path,
];
if transcode_video {
args.extend([
"-pix_fmt", "-pix_fmt",
output_format.pix_fmt(), output_format.pix_fmt(),
"-vf", "-vf",
"scale=trunc(iw/2)*2:trunc(ih/2)*2", "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.extend(["-c:v", "copy"]);
}
if transcode_audio {
if let Some(audio_codec) = output_format.ffmpeg_audio_codec() { if let Some(audio_codec) = output_format.ffmpeg_audio_codec() {
args.extend(["-c:a", audio_codec]); args.extend(["-c:a", audio_codec]);
} else { } else {
args.push("-an") args.push("-an")
} }
} else {
args.extend(["-c:v", output_format.ffmpeg_video_codec()]); args.extend(["-c:a", "copy"]);
if output_format.is_vp9() {
args.extend(["-b:v", "0"]);
} }
let crf = crf.to_string(); args.extend(["-f", output_format.ffmpeg_format(), output_path]);
args.extend([
"-crf",
&crf,
"-f",
output_format.ffmpeg_format(),
output_path,
]);
Process::run("ffmpeg", &args, timeout)?.wait().await?; Process::run("ffmpeg", &args, timeout)?.wait().await?;

View File

@ -2,7 +2,7 @@ use actix_web::web::Bytes;
use tokio::io::AsyncRead; use tokio::io::AsyncRead;
use crate::{ use crate::{
formats::{AnimationFormat, ImageFormat, OutputVideoFormat}, formats::{AnimationFormat, ImageFormat},
magick::MagickError, magick::MagickError,
process::Process, process::Process,
}; };
@ -43,23 +43,6 @@ pub(super) async fn convert_animation(
.await .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( async fn convert(
input: &'static str, input: &'static str,
output: &'static str, output: &'static str,