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,