From b48a9233b275e0ff8ec62039e1c7b9664e8e722c Mon Sep 17 00:00:00 2001 From: asonix Date: Wed, 30 Aug 2023 20:37:54 -0500 Subject: [PATCH] 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 --- src/config/defaults.rs | 3 - src/config/file.rs | 3 +- src/details.rs | 37 +- src/discover.rs | 48 +- src/discover/ffmpeg.rs | 488 ++++++++++-------- .../ffprobe_6_0_animated_avif_details.json | 17 + .../ffprobe_6_0_animated_webp_details.json | 1 + .../ffmpeg/ffprobe_6_0_apng_details.json | 2 + .../ffmpeg/ffprobe_6_0_avif_details.json | 6 +- .../ffmpeg/ffprobe_6_0_gif_details.json | 8 +- .../ffmpeg/ffprobe_6_0_jpeg_details.json | 6 +- .../ffmpeg/ffprobe_6_0_jxl_details.json | 1 + .../ffmpeg/ffprobe_6_0_mp4_av1_details.json | 17 + .../ffmpeg/ffprobe_6_0_mp4_details.json | 8 +- .../ffmpeg/ffprobe_6_0_png_details.json | 2 + .../ffmpeg/ffprobe_6_0_webm_av1_details.json | 2 + .../ffmpeg/ffprobe_6_0_webm_details.json | 8 +- .../ffmpeg/ffprobe_6_0_webp_details.json | 6 +- src/discover/ffmpeg/tests.rs | 138 +++-- src/discover/magick.rs | 90 +--- src/discover/magick/tests.rs | 69 ++- src/formats.rs | 5 +- src/formats/video.rs | 467 ++++++++++------- src/validate.rs | 60 +-- src/validate/ffmpeg.rs | 63 ++- src/validate/magick.rs | 19 +- 26 files changed, 858 insertions(+), 716 deletions(-) create mode 100644 src/discover/ffmpeg/ffprobe_6_0_animated_avif_details.json create mode 100644 src/discover/ffmpeg/ffprobe_6_0_mp4_av1_details.json diff --git a/src/config/defaults.rs b/src/config/defaults.rs index 9642e0c..d47ead0 100644 --- a/src/config/defaults.rs +++ b/src/config/defaults.rs @@ -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(), } } diff --git a/src/config/file.rs b/src/config/file.rs index f1e66a5..9e78d19 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -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, pub(crate) quality: VideoQuality, diff --git a/src/details.rs b/src/details.rs index 86b752a..f494a22 100644 --- a/src/details.rs +++ b/src/details.rs @@ -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 { - 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( @@ -50,14 +57,20 @@ impl Details { identifier: &S::Identifier, timeout: u64, ) -> Result { - 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 { diff --git a/src/discover.rs b/src/discover.rs index 4234fd0..acbe53f 100644 --- a/src/discover.rs +++ b/src/discover.rs @@ -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, } -#[derive(Debug, PartialEq, Eq)] -pub(crate) struct DiscoveryLite { - pub(crate) format: InternalFormat, - pub(crate) width: u16, - pub(crate) height: u16, - pub(crate) frames: Option, -} - #[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 { - 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( - store: &S, - identifier: &S::Identifier, - timeout: u64, -) -> Result -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, diff --git a/src/discover/ffmpeg.rs b/src/discover/ffmpeg.rs index 50d3624..6700742 100644 --- a/src/discover/ffmpeg.rs +++ b/src/discover/ffmpeg.rs @@ -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, +} + +impl FfMpegStreams { + fn into_parts(self) -> Option<(FfMpegVideoStream, Option)> { + 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(deserializer: D) -> Result + 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, + pix_fmt: Option, + nb_read_frames: Option, +} + +#[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, 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, 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 { + static ALPHA_PIXEL_FORMATS: OnceLock> = OnceLock::new(); -pub(super) async fn discover_stream_lite( - timeout: u64, - stream: S, -) -> Result, FfMpegError> -where - S: Stream> + 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: F, - timeout: u64, -) -> Result, FfMpegError> -where - F: FnOnce(crate::file::File) -> Fut, - Fut: std::future::Future>, -{ - 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: F, timeout: u64) -> Result, FfMpegError> -where - F: Fn(crate::file::File) -> Fut + Clone, - Fut: std::future::Future>, -{ - 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> = 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: F, timeout: u64) -> Result, FfMpegError> +async fn discover_file(f: F, timeout: u64) -> Result, FfMpegError> where F: FnOnce(crate::file::File) -> Fut, Fut: std::future::Future>, @@ -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: F, timeout: u64) -> Result -where - F: FnOnce(crate::file::File) -> Fut, - Fut: std::future::Future>, -{ - 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, FfMpegError> { @@ -346,56 +299,145 @@ fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet { .collect() } -fn parse_discovery(discovery: FfMpegDiscovery) -> Result, FfMpegError> { +fn is_mp4(format_name: &str) -> bool { + format_name.contains(MP4) +} + +fn mp4_audio_codec(stream: Option) -> Option { + match stream { + Some(FfMpegAudioStream { + codec_name: FfMpegAudioCodec::Aac, + }) => Some(Mp4AudioCodec::Aac), + _ => None, + } +} + +fn webm_audio_codec(stream: Option) -> Option { + 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, Option), 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, + )) } diff --git a/src/discover/ffmpeg/ffprobe_6_0_animated_avif_details.json b/src/discover/ffmpeg/ffprobe_6_0_animated_avif_details.json new file mode 100644 index 0000000..0ed8103 --- /dev/null +++ b/src/discover/ffmpeg/ffprobe_6_0_animated_avif_details.json @@ -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" + } +} diff --git a/src/discover/ffmpeg/ffprobe_6_0_animated_webp_details.json b/src/discover/ffmpeg/ffprobe_6_0_animated_webp_details.json index 8fb1dce..824e51b 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_animated_webp_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_animated_webp_details.json @@ -4,6 +4,7 @@ ], "streams": [ { + "codec_name": "webp", "width": 0, "height": 0 } diff --git a/src/discover/ffmpeg/ffprobe_6_0_apng_details.json b/src/discover/ffmpeg/ffprobe_6_0_apng_details.json index 8deac89..93418d6 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_apng_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_apng_details.json @@ -4,8 +4,10 @@ ], "streams": [ { + "codec_name": "apng", "width": 112, "height": 112, + "pix_fmt": "rgba", "nb_read_frames": "27" } ], diff --git a/src/discover/ffmpeg/ffprobe_6_0_avif_details.json b/src/discover/ffmpeg/ffprobe_6_0_avif_details.json index daa368b..b2d9f14 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_avif_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_avif_details.json @@ -4,8 +4,10 @@ ], "streams": [ { - "width": 1920, - "height": 1080, + "codec_name": "av1", + "width": 1200, + "height": 1387, + "pix_fmt": "yuv420p", "nb_read_frames": "1" } ], diff --git a/src/discover/ffmpeg/ffprobe_6_0_gif_details.json b/src/discover/ffmpeg/ffprobe_6_0_gif_details.json index 73a3a92..df2a753 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_gif_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_gif_details.json @@ -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": { diff --git a/src/discover/ffmpeg/ffprobe_6_0_jpeg_details.json b/src/discover/ffmpeg/ffprobe_6_0_jpeg_details.json index 57873fe..c2f3acc 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_jpeg_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_jpeg_details.json @@ -4,8 +4,10 @@ ], "streams": [ { - "width": 1920, - "height": 1080, + "codec_name": "mjpeg", + "width": 1663, + "height": 1247, + "pix_fmt": "yuvj420p", "nb_read_frames": "1" } ], diff --git a/src/discover/ffmpeg/ffprobe_6_0_jxl_details.json b/src/discover/ffmpeg/ffprobe_6_0_jxl_details.json index 847d383..ccba9fb 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_jxl_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_jxl_details.json @@ -4,6 +4,7 @@ ], "streams": [ { + "codec_name": "jpegxl", "width": 0, "height": 0 } diff --git a/src/discover/ffmpeg/ffprobe_6_0_mp4_av1_details.json b/src/discover/ffmpeg/ffprobe_6_0_mp4_av1_details.json new file mode 100644 index 0000000..47d3b75 --- /dev/null +++ b/src/discover/ffmpeg/ffprobe_6_0_mp4_av1_details.json @@ -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" + } +} diff --git a/src/discover/ffmpeg/ffprobe_6_0_mp4_details.json b/src/discover/ffmpeg/ffprobe_6_0_mp4_details.json index d68714b..9300a69 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_mp4_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_mp4_details.json @@ -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": { diff --git a/src/discover/ffmpeg/ffprobe_6_0_png_details.json b/src/discover/ffmpeg/ffprobe_6_0_png_details.json index f904a38..cc2d98a 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_png_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_png_details.json @@ -4,8 +4,10 @@ ], "streams": [ { + "codec_name": "png", "width": 450, "height": 401, + "pix_fmt": "rgb24", "nb_read_frames": "1" } ], diff --git a/src/discover/ffmpeg/ffprobe_6_0_webm_av1_details.json b/src/discover/ffmpeg/ffprobe_6_0_webm_av1_details.json index 0835275..ac7c80c 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_webm_av1_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_webm_av1_details.json @@ -4,8 +4,10 @@ ], "streams": [ { + "codec_name": "av1", "width": 112, "height": 112, + "pix_fmt": "gbrp", "nb_read_frames": "27" } ], diff --git a/src/discover/ffmpeg/ffprobe_6_0_webm_details.json b/src/discover/ffmpeg/ffprobe_6_0_webm_details.json index 013ba3f..0c8c75a 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_webm_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_webm_details.json @@ -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": { diff --git a/src/discover/ffmpeg/ffprobe_6_0_webp_details.json b/src/discover/ffmpeg/ffprobe_6_0_webp_details.json index 4a5397f..b676414 100644 --- a/src/discover/ffmpeg/ffprobe_6_0_webp_details.json +++ b/src/discover/ffmpeg/ffprobe_6_0_webp_details.json @@ -4,8 +4,10 @@ ], "streams": [ { - "width": 1920, - "height": 1080, + "codec_name": "webp", + "width": 1200, + "height": 1387, + "pix_fmt": "yuv420p", "nb_read_frames": "1" } ], diff --git a/src/discover/ffmpeg/tests.rs b/src/discover/ffmpeg/tests.rs index 334b9dd..73d994d 100644 --- a/src/discover/ffmpeg/tests.rs +++ b/src/discover/ffmpeg/tests.rs @@ -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); 11] { +fn details_tests() -> [(&'static str, Option); 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); 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); 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); 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); } diff --git a/src/discover/magick.rs b/src/discover/magick.rs index 88df362..554c812 100644 --- a/src/discover/magick.rs +++ b/src/discover/magick.rs @@ -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 { - 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( - timeout: u64, - stream: S, -) -> Result -where - S: Stream> + 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, 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: F, timeout: u64) -> Result -where - F: FnOnce(crate::file::File) -> Fut, - Fut: std::future::Future>, -{ - discover_file(f, timeout).await.map(Discovery::lite) -} - async fn discover_file(f: F, timeout: u64) -> Result where F: FnOnce(crate::file::File) -> Fut, @@ -338,12 +288,6 @@ fn parse_discovery(output: Vec) -> Result 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) -> Result Ok(Discovery { - input: InputFile::Video(VideoFormat::Webm { alpha: true }), - width, - height, - frames: Some(frames), - }), otherwise => Err(DiscoverError::UnsupportedFileType(String::from(otherwise))), } } diff --git a/src/discover/magick/tests.rs b/src/discover/magick/tests.rs index 765faeb..5149d3e 100644 --- a/src/discover/magick/tests.rs +++ b/src/discover/magick/tests.rs @@ -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 = 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); } diff --git a/src/formats.rs b/src/formats.rs index 9fe9310..eccef38 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -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( diff --git a/src/formats/video.rs b/src/formats/video.rs index 4fe0731..2ed8dd0 100644 --- a/src/formats/video.rs +++ b/src/formats/video.rs @@ -1,7 +1,20 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum InputVideoFormat { + Mp4 { + video_codec: Mp4Codec, + audio_codec: Option, + }, + Webm { + video_codec: WebmCodec, + audio_codec: Option, + }, +} + #[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, + provided: Option, +) -> (Option, 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, + provided: Option, +) -> (Option, 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, + 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, + 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, + 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, + prescribed_video_codec: Option, + prescribed_audio_codec: Option, 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, - 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, diff --git a/src/validate.rs b/src/validate.rs index 1b3e58b..5d42199 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -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)) } diff --git a/src/validate/ffmpeg.rs b/src/validate/ffmpeg.rs index 7954a61..9184ea1 100644 --- a/src/validate/ffmpeg.rs +++ b/src/validate/ffmpeg.rs @@ -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?; diff --git a/src/validate/magick.rs b/src/validate/magick.rs index 67ddeb1..0a9f7fd 100644 --- a/src/validate/magick.rs +++ b/src/validate/magick.rs @@ -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 { - convert( - input.magick_format(), - output.magick_format(), - true, - None, - timeout, - bytes, - ) - .await -} - async fn convert( input: &'static str, output: &'static str,