mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-11-09 22:14:59 +00:00
Support audio in uploaded videos, allow webm uploads
This commit is contained in:
parent
c57a48db8a
commit
890478e794
7 changed files with 77 additions and 20 deletions
|
@ -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
|
||||||
|
|
12
pict-rs.toml
12
pict-rs.toml
|
@ -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']
|
||||||
|
|
|
@ -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![
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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?,
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue