Support audio in uploaded videos, allow webm uploads

This commit is contained in:
asonix 2022-09-25 18:16:37 -05:00
parent c57a48db8a
commit 890478e794
7 changed files with 77 additions and 20 deletions

View File

@ -20,7 +20,9 @@ max_width = 10000
max_height = 10000 max_height = 10000
max_area = 40000000 max_area = 40000000
max_file_size = 40 max_file_size = 40
max_frame_count = 900
enable_silent_video = true enable_silent_video = true
enable_full_video = false
filters = ['blur', 'crop', 'identity', 'resize', 'thumbnail'] filters = ['blur', 'crop', 'identity', 'resize', 'thumbnail']
skip_validate_imports = false skip_validate_imports = false
cache_duration = 168 cache_duration = 168

View File

@ -142,13 +142,23 @@ max_area = 40000000
# default: 40 # default: 40
max_file_size = 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 # environment variable: PICTRS__MEDIA__ENABLE_SILENT_VIDEO
# default: true # default: true
# #
# Set this to false to serve static images only # Set this to false to serve static images only
enable_silent_video = true 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 ## Optional: set allowed filters for image processing
# environment variable: PICTRS__MEDIA__FILTERS # environment variable: PICTRS__MEDIA__FILTERS
# default: ['blur', 'crop', 'identity', 'resize', 'thumbnail'] # default: ['blur', 'crop', 'identity', 'resize', 'thumbnail']

View File

@ -152,7 +152,7 @@ impl Default for MediaDefaults {
max_height: 10_000, max_height: 10_000,
max_area: 40_000_000, max_area: 40_000_000,
max_file_size: 40, max_file_size: 40,
max_frame_count: 3_600, max_frame_count: 900,
enable_silent_video: true, enable_silent_video: true,
enable_full_video: false, enable_full_video: false,
filters: vec![ filters: vec![

View File

@ -54,6 +54,7 @@ impl ThumbnailFormat {
pub(crate) async fn to_mp4_bytes( pub(crate) async fn to_mp4_bytes(
input: Bytes, input: Bytes,
input_format: InputFormat, input_format: InputFormat,
permit_audio: bool,
) -> Result<impl AsyncRead + Unpin, Error> { ) -> Result<impl AsyncRead + Unpin, Error> {
let input_file = crate::tmp_file::tmp_file(Some(input_format.to_ext())); 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)?; 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.write_from_bytes(input).await?;
tmp_one.close().await?; tmp_one.close().await?;
let process = Process::run( let process = if permit_audio {
"ffmpeg", 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", "-i",
input_file_str, input_file_str,
"-pix_fmt", "-pix_fmt",
@ -77,13 +93,13 @@ pub(crate) async fn to_mp4_bytes(
"-vf", "-vf",
"scale=trunc(iw/2)*2:trunc(ih/2)*2", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
"-an", "-an",
"-codec", "-c:v",
"h264", "h264",
"-f", "-f",
"mp4", "mp4",
output_file_str, output_file_str,
], ])?
)?; };
process.wait().await?; process.wait().await?;
tokio::fs::remove_file(input_file).await?; tokio::fs::remove_file(input_file).await?;

View File

@ -74,6 +74,7 @@ where
bytes, bytes,
CONFIG.media.format, CONFIG.media.format,
CONFIG.media.enable_silent_video, CONFIG.media.enable_silent_video,
CONFIG.media.enable_full_video,
should_validate, should_validate,
) )
.await?; .await?;

View File

@ -16,22 +16,29 @@ pub(crate) fn details_hint(alias: &Alias) -> Option<ValidInputType> {
let ext = alias.extension()?; let ext = alias.extension()?;
if ext.ends_with(".mp4") { if ext.ends_with(".mp4") {
Some(ValidInputType::Mp4) Some(ValidInputType::Mp4)
} else if ext.ends_with(".webm") {
Some(ValidInputType::Webm)
} else { } else {
None None
} }
} }
pub(crate) fn image_webp() -> mime::Mime { fn image_webp() -> mime::Mime {
"image/webp".parse().unwrap() "image/webp".parse().unwrap()
} }
pub(crate) fn video_mp4() -> mime::Mime { fn video_mp4() -> mime::Mime {
"video/mp4".parse().unwrap() "video/mp4".parse().unwrap()
} }
fn video_webm() -> mime::Mime {
"video/webm".parse().unwrap()
}
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub(crate) enum ValidInputType { pub(crate) enum ValidInputType {
Mp4, Mp4,
Webm,
Gif, Gif,
Png, Png,
Jpeg, Jpeg,
@ -42,6 +49,7 @@ impl ValidInputType {
fn as_str(self) -> &'static str { fn as_str(self) -> &'static str {
match self { match self {
Self::Mp4 => "MP4", Self::Mp4 => "MP4",
Self::Webm => "WEBM",
Self::Gif => "GIF", Self::Gif => "GIF",
Self::Png => "PNG", Self::Png => "PNG",
Self::Jpeg => "JPEG", Self::Jpeg => "JPEG",
@ -52,6 +60,7 @@ impl ValidInputType {
pub(crate) fn as_ext(self) -> &'static str { pub(crate) fn as_ext(self) -> &'static str {
match self { match self {
Self::Mp4 => ".mp4", Self::Mp4 => ".mp4",
Self::Webm => ".webm",
Self::Gif => ".gif", Self::Gif => ".gif",
Self::Png => ".png", Self::Png => ".png",
Self::Jpeg => ".jpeg", Self::Jpeg => ".jpeg",
@ -59,8 +68,13 @@ impl ValidInputType {
} }
} }
fn is_mp4(self) -> bool { fn video_hint(self) -> Option<&'static str> {
matches!(self, Self::Mp4) match self {
Self::Mp4 => Some(".mp4"),
Self::Webm => Some(".webm"),
Self::Gif => Some(".gif"),
_ => None,
}
} }
pub(crate) fn from_format(format: ImageFormat) -> Self { pub(crate) fn from_format(format: ImageFormat) -> Self {
@ -119,8 +133,8 @@ pub(crate) async fn details_bytes(
input: Bytes, input: Bytes,
hint: Option<ValidInputType>, hint: Option<ValidInputType>,
) -> Result<Details, Error> { ) -> Result<Details, Error> {
if hint.as_ref().map(|h| h.is_mp4()).unwrap_or(false) { if let Some(hint) = hint.and_then(|hint| hint.video_hint()) {
let input_file = crate::tmp_file::tmp_file(Some(".mp4")); let input_file = crate::tmp_file::tmp_file(Some(hint));
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?; let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
crate::store::file_store::safe_create_parent(&input_file).await?; crate::store::file_store::safe_create_parent(&input_file).await?;
@ -157,8 +171,8 @@ pub(crate) async fn details_store<S: Store + 'static>(
identifier: S::Identifier, identifier: S::Identifier,
hint: Option<ValidInputType>, hint: Option<ValidInputType>,
) -> Result<Details, Error> { ) -> Result<Details, Error> {
if hint.as_ref().map(|h| h.is_mp4()).unwrap_or(false) { if let Some(hint) = hint.and_then(|hint| hint.video_hint()) {
let input_file = crate::tmp_file::tmp_file(Some(".mp4")); let input_file = crate::tmp_file::tmp_file(Some(hint));
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?; let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
crate::store::file_store::safe_create_parent(&input_file).await?; crate::store::file_store::safe_create_parent(&input_file).await?;
@ -249,6 +263,7 @@ fn parse_details(s: std::borrow::Cow<'_, str>) -> Result<Details, Error> {
let mime_type = match format { let mime_type = match format {
"MP4" => video_mp4(), "MP4" => video_mp4(),
"WEBM" => video_webm(),
"GIF" => mime::IMAGE_GIF, "GIF" => mime::IMAGE_GIF,
"PNG" => mime::IMAGE_PNG, "PNG" => mime::IMAGE_PNG,
"JPEG" => mime::IMAGE_JPEG, "JPEG" => mime::IMAGE_JPEG,
@ -323,6 +338,7 @@ impl Details {
let input_type = match (self.mime_type.type_(), self.mime_type.subtype()) { let input_type = match (self.mime_type.type_(), self.mime_type.subtype()) {
(mime::VIDEO, mime::MP4 | mime::MPEG) => ValidInputType::Mp4, (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::GIF) => ValidInputType::Gif,
(mime::IMAGE, mime::PNG) => ValidInputType::Png, (mime::IMAGE, mime::PNG) => ValidInputType::Png,
(mime::IMAGE, mime::JPEG) => ValidInputType::Jpeg, (mime::IMAGE, mime::JPEG) => ValidInputType::Jpeg,

View File

@ -41,6 +41,7 @@ pub(crate) async fn validate_image_bytes(
bytes: Bytes, bytes: Bytes,
prescribed_format: Option<ImageFormat>, prescribed_format: Option<ImageFormat>,
enable_silent_video: bool, enable_silent_video: bool,
enable_full_video: bool,
validate: bool, validate: bool,
) -> Result<(ValidInputType, impl AsyncRead + Unpin), Error> { ) -> Result<(ValidInputType, impl AsyncRead + Unpin), Error> {
let input_type = crate::magick::input_type_bytes(bytes.clone()).await?; 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) { match (prescribed_format, input_type) {
(_, ValidInputType::Gif) => { (_, ValidInputType::Gif) => {
if !enable_silent_video { if !(enable_silent_video || enable_full_video) {
return Err(UploadError::SilentVideoDisabled.into()); return Err(UploadError::SilentVideoDisabled.into());
} }
Ok(( Ok((
ValidInputType::Mp4, ValidInputType::Mp4,
Either::right(Either::left( 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) => { (_, ValidInputType::Mp4) => {
if !enable_silent_video { if !(enable_silent_video || enable_full_video) {
return Err(UploadError::SilentVideoDisabled.into()); return Err(UploadError::SilentVideoDisabled.into());
} }
Ok(( Ok((
ValidInputType::Mp4, ValidInputType::Mp4,
Either::right(Either::left( 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?,
)), )),
)) ))
} }