Support audio in uploaded videos, allow webm uploads
All checks were successful
continuous-integration/drone/push Build is passing

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_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

View file

@ -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']

View file

@ -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![

View file

@ -54,6 +54,7 @@ impl ThumbnailFormat {
pub(crate) async fn to_mp4_bytes(
input: Bytes,
input_format: InputFormat,
permit_audio: bool,
) -> Result<impl AsyncRead + Unpin, Error> {
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?;

View file

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

View file

@ -16,22 +16,29 @@ pub(crate) fn details_hint(alias: &Alias) -> Option<ValidInputType> {
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<ValidInputType>,
) -> Result<Details, Error> {
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<S: Store + 'static>(
identifier: S::Identifier,
hint: Option<ValidInputType>,
) -> Result<Details, Error> {
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<Details, Error> {
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,

View file

@ -41,6 +41,7 @@ pub(crate) async fn validate_image_bytes(
bytes: Bytes,
prescribed_format: Option<ImageFormat>,
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?,
)),
))
}