From 890478e794b06bdff62afcd24ca6818683d8b1d9 Mon Sep 17 00:00:00 2001 From: asonix Date: Sun, 25 Sep 2022 18:16:37 -0500 Subject: [PATCH] Support audio in uploaded videos, allow webm uploads --- defaults.toml | 2 ++ pict-rs.toml | 12 +++++++++++- src/config/defaults.rs | 2 +- src/ffmpeg.rs | 28 ++++++++++++++++++++++------ src/ingest.rs | 1 + src/magick.rs | 32 ++++++++++++++++++++++++-------- src/validate.rs | 20 ++++++++++++++++---- 7 files changed, 77 insertions(+), 20 deletions(-) diff --git a/defaults.toml b/defaults.toml index 91606e0..c71f134 100644 --- a/defaults.toml +++ b/defaults.toml @@ -20,7 +20,9 @@ max_width = 10000 max_height = 10000 max_area = 40000000 max_file_size = 40 +max_frame_count = 900 enable_silent_video = true +enable_full_video = false filters = ['blur', 'crop', 'identity', 'resize', 'thumbnail'] skip_validate_imports = false cache_duration = 168 diff --git a/pict-rs.toml b/pict-rs.toml index 45981ed..9371d9e 100644 --- a/pict-rs.toml +++ b/pict-rs.toml @@ -142,13 +142,23 @@ max_area = 40000000 # default: 40 max_file_size = 40 -## Optional: enable GIF and MP4 uploads (without sound) +## Optional: max frame count +# environment variable: PICTRS__MEDIA__MAX_FRAME_COUNT +# default: # 900 +max_frame_count = 900 + +## Optional: enable GIF, MP4, and WEBM uploads (without sound) # environment variable: PICTRS__MEDIA__ENABLE_SILENT_VIDEO # default: true # # Set this to false to serve static images only enable_silent_video = true +## Optional: enable MP4, and WEBM uploads (with sound) and GIF (without sound) +# environment variable: PICTRS__MEDIA__ENABLE_FULL_VIDEO +# default: false +enable_full_video = false + ## Optional: set allowed filters for image processing # environment variable: PICTRS__MEDIA__FILTERS # default: ['blur', 'crop', 'identity', 'resize', 'thumbnail'] diff --git a/src/config/defaults.rs b/src/config/defaults.rs index fce6f28..7873907 100644 --- a/src/config/defaults.rs +++ b/src/config/defaults.rs @@ -152,7 +152,7 @@ impl Default for MediaDefaults { max_height: 10_000, max_area: 40_000_000, max_file_size: 40, - max_frame_count: 3_600, + max_frame_count: 900, enable_silent_video: true, enable_full_video: false, filters: vec![ diff --git a/src/ffmpeg.rs b/src/ffmpeg.rs index 101fa29..2452e33 100644 --- a/src/ffmpeg.rs +++ b/src/ffmpeg.rs @@ -54,6 +54,7 @@ impl ThumbnailFormat { pub(crate) async fn to_mp4_bytes( input: Bytes, input_format: InputFormat, + permit_audio: bool, ) -> Result { let input_file = crate::tmp_file::tmp_file(Some(input_format.to_ext())); let input_file_str = input_file.to_str().ok_or(UploadError::Path)?; @@ -67,9 +68,24 @@ pub(crate) async fn to_mp4_bytes( tmp_one.write_from_bytes(input).await?; tmp_one.close().await?; - let process = Process::run( - "ffmpeg", - &[ + let process = if permit_audio { + Process::run("ffmpeg", &[ + "-i", + input_file_str, + "-pix_fmt", + "yuv420p", + "-vf", + "scale=trunc(iw/2)*2:trunc(ih/2)*2", + "-c:a", + "aac", + "-c:v", + "h264", + "-f", + "mp4", + output_file_str, + ])? + } else { + Process::run("ffmpeg", &[ "-i", input_file_str, "-pix_fmt", @@ -77,13 +93,13 @@ pub(crate) async fn to_mp4_bytes( "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", "-an", - "-codec", + "-c:v", "h264", "-f", "mp4", output_file_str, - ], - )?; + ])? + }; process.wait().await?; tokio::fs::remove_file(input_file).await?; diff --git a/src/ingest.rs b/src/ingest.rs index 7905c90..04c3562 100644 --- a/src/ingest.rs +++ b/src/ingest.rs @@ -74,6 +74,7 @@ where bytes, CONFIG.media.format, CONFIG.media.enable_silent_video, + CONFIG.media.enable_full_video, should_validate, ) .await?; diff --git a/src/magick.rs b/src/magick.rs index afb77b8..ca37b22 100644 --- a/src/magick.rs +++ b/src/magick.rs @@ -16,22 +16,29 @@ pub(crate) fn details_hint(alias: &Alias) -> Option { let ext = alias.extension()?; if ext.ends_with(".mp4") { Some(ValidInputType::Mp4) + } else if ext.ends_with(".webm") { + Some(ValidInputType::Webm) } else { None } } -pub(crate) fn image_webp() -> mime::Mime { +fn image_webp() -> mime::Mime { "image/webp".parse().unwrap() } -pub(crate) fn video_mp4() -> mime::Mime { +fn video_mp4() -> mime::Mime { "video/mp4".parse().unwrap() } +fn video_webm() -> mime::Mime { + "video/webm".parse().unwrap() +} + #[derive(Copy, Clone, Debug)] pub(crate) enum ValidInputType { Mp4, + Webm, Gif, Png, Jpeg, @@ -42,6 +49,7 @@ impl ValidInputType { fn as_str(self) -> &'static str { match self { Self::Mp4 => "MP4", + Self::Webm => "WEBM", Self::Gif => "GIF", Self::Png => "PNG", Self::Jpeg => "JPEG", @@ -52,6 +60,7 @@ impl ValidInputType { pub(crate) fn as_ext(self) -> &'static str { match self { Self::Mp4 => ".mp4", + Self::Webm => ".webm", Self::Gif => ".gif", Self::Png => ".png", Self::Jpeg => ".jpeg", @@ -59,8 +68,13 @@ impl ValidInputType { } } - fn is_mp4(self) -> bool { - matches!(self, Self::Mp4) + fn video_hint(self) -> Option<&'static str> { + match self { + Self::Mp4 => Some(".mp4"), + Self::Webm => Some(".webm"), + Self::Gif => Some(".gif"), + _ => None, + } } pub(crate) fn from_format(format: ImageFormat) -> Self { @@ -119,8 +133,8 @@ pub(crate) async fn details_bytes( input: Bytes, hint: Option, ) -> Result { - if hint.as_ref().map(|h| h.is_mp4()).unwrap_or(false) { - let input_file = crate::tmp_file::tmp_file(Some(".mp4")); + if let Some(hint) = hint.and_then(|hint| hint.video_hint()) { + let input_file = crate::tmp_file::tmp_file(Some(hint)); let input_file_str = input_file.to_str().ok_or(UploadError::Path)?; crate::store::file_store::safe_create_parent(&input_file).await?; @@ -157,8 +171,8 @@ pub(crate) async fn details_store( identifier: S::Identifier, hint: Option, ) -> Result { - if hint.as_ref().map(|h| h.is_mp4()).unwrap_or(false) { - let input_file = crate::tmp_file::tmp_file(Some(".mp4")); + if let Some(hint) = hint.and_then(|hint| hint.video_hint()) { + let input_file = crate::tmp_file::tmp_file(Some(hint)); let input_file_str = input_file.to_str().ok_or(UploadError::Path)?; crate::store::file_store::safe_create_parent(&input_file).await?; @@ -249,6 +263,7 @@ fn parse_details(s: std::borrow::Cow<'_, str>) -> Result { let mime_type = match format { "MP4" => video_mp4(), + "WEBM" => video_webm(), "GIF" => mime::IMAGE_GIF, "PNG" => mime::IMAGE_PNG, "JPEG" => mime::IMAGE_JPEG, @@ -323,6 +338,7 @@ impl Details { let input_type = match (self.mime_type.type_(), self.mime_type.subtype()) { (mime::VIDEO, mime::MP4 | mime::MPEG) => ValidInputType::Mp4, + (mime::VIDEO, subtype) if subtype.as_str() == "webm" => ValidInputType::Webm, (mime::IMAGE, mime::GIF) => ValidInputType::Gif, (mime::IMAGE, mime::PNG) => ValidInputType::Png, (mime::IMAGE, mime::JPEG) => ValidInputType::Jpeg, diff --git a/src/validate.rs b/src/validate.rs index 0a78159..316a340 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -41,6 +41,7 @@ pub(crate) async fn validate_image_bytes( bytes: Bytes, prescribed_format: Option, enable_silent_video: bool, + enable_full_video: bool, validate: bool, ) -> Result<(ValidInputType, impl AsyncRead + Unpin), Error> { let input_type = crate::magick::input_type_bytes(bytes.clone()).await?; @@ -51,24 +52,35 @@ pub(crate) async fn validate_image_bytes( match (prescribed_format, input_type) { (_, ValidInputType::Gif) => { - if !enable_silent_video { + if !(enable_silent_video || enable_full_video) { return Err(UploadError::SilentVideoDisabled.into()); } Ok(( ValidInputType::Mp4, Either::right(Either::left( - crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Gif).await?, + crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Gif, false).await?, )), )) } (_, ValidInputType::Mp4) => { - if !enable_silent_video { + if !(enable_silent_video || enable_full_video) { return Err(UploadError::SilentVideoDisabled.into()); } Ok(( ValidInputType::Mp4, Either::right(Either::left( - crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Mp4).await?, + crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Mp4, enable_full_video).await?, + )), + )) + } + (_, ValidInputType::Webm) => { + if !(enable_silent_video || enable_full_video) { + return Err(UploadError::SilentVideoDisabled.into()); + } + Ok(( + ValidInputType::Mp4, + Either::right(Either::left( + crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Mp4, enable_full_video).await?, )), )) }