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::{
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(),
}
}

View file

@ -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,

View file

@ -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 {

View file

@ -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,

View file

@ -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,
))
}

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": [
{
"codec_name": "webp",
"width": 0,
"height": 0
}

View file

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

View file

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

View file

@ -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": {

View file

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

View file

@ -4,6 +4,7 @@
],
"streams": [
{
"codec_name": "jpegxl",
"width": 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": [
{
"width": 852,
"height": 480,
"nb_read_frames": "35364"
"codec_name": "h264",
"width": 1426,
"height": 834,
"pix_fmt": "yuv420p",
"nb_read_frames": "105"
}
],
"format": {

View file

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

View file

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

View file

@ -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": {

View file

@ -4,8 +4,10 @@
],
"streams": [
{
"width": 1920,
"height": 1080,
"codec_name": "webp",
"width": 1200,
"height": 1387,
"pix_fmt": "yuv420p",
"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",
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);
}

View file

@ -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))),
}
}

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",
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);
}

View file

@ -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(

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)]
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,

View file

@ -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))
}

View file

@ -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?;

View file

@ -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,