mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 19:31:35 +00:00
Allow uploading small gifs
This commit is contained in:
parent
d72653cf78
commit
40f57be0c7
10 changed files with 324 additions and 106 deletions
|
@ -1,20 +1,20 @@
|
||||||
[server]
|
[server]
|
||||||
address = '0.0.0.0:8080'
|
address = "0.0.0.0:8080"
|
||||||
worker_id = 'pict-rs-1'
|
worker_id = "pict-rs-1"
|
||||||
|
|
||||||
[tracing.logging]
|
[tracing.logging]
|
||||||
format = 'normal'
|
format = "normal"
|
||||||
targets = 'warn,tracing_actix_web=info,actix_server=info,actix_web=info'
|
targets = "warn,tracing_actix_web=info,actix_server=info,actix_web=info"
|
||||||
|
|
||||||
[tracing.console]
|
[tracing.console]
|
||||||
buffer_capacity = 102400
|
buffer_capacity = 102400
|
||||||
|
|
||||||
[tracing.opentelemetry]
|
[tracing.opentelemetry]
|
||||||
service_name = 'pict-rs'
|
service_name = "pict-rs"
|
||||||
targets = 'info'
|
targets = "info"
|
||||||
|
|
||||||
[old_db]
|
[old_db]
|
||||||
path = '/mnt'
|
path = "/mnt"
|
||||||
|
|
||||||
[media]
|
[media]
|
||||||
max_width = 10000
|
max_width = 10000
|
||||||
|
@ -24,16 +24,27 @@ max_file_size = 40
|
||||||
max_frame_count = 900
|
max_frame_count = 900
|
||||||
enable_silent_video = true
|
enable_silent_video = true
|
||||||
enable_full_video = false
|
enable_full_video = false
|
||||||
video_codec = 'vp9'
|
video_codec = "vp9"
|
||||||
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
|
||||||
|
|
||||||
|
[media.gif]
|
||||||
|
max_width = 128
|
||||||
|
max_height = 128
|
||||||
|
max_area = 16384
|
||||||
|
|
||||||
[repo]
|
[repo]
|
||||||
type = 'sled'
|
type = "sled"
|
||||||
path = '/mnt/sled-repo'
|
path = "/mnt/sled-repo"
|
||||||
cache_capacity = 67108864
|
cache_capacity = 67108864
|
||||||
|
|
||||||
[store]
|
[store]
|
||||||
type = 'filesystem'
|
type = "filesystem"
|
||||||
path = '/mnt/files'
|
path = "/mnt/files"
|
||||||
|
|
28
pict-rs.toml
28
pict-rs.toml
|
@ -196,6 +196,34 @@ skip_validate_imports = false
|
||||||
# default: 168 (1 week)
|
# default: 168 (1 week)
|
||||||
cache_duration = 168
|
cache_duration = 168
|
||||||
|
|
||||||
|
## Gif configuration
|
||||||
|
#
|
||||||
|
# Making any of these bounds 0 will disable gif uploads
|
||||||
|
[media.gif]
|
||||||
|
# Optional: Maximum width in pixels for uploaded gifs
|
||||||
|
# environment variable: PICTRS__MEDIA__GIF__MAX_WIDTH
|
||||||
|
# default: 128
|
||||||
|
#
|
||||||
|
# If a gif does not fit within this bound, it will either be transcoded to a video or rejected,
|
||||||
|
# depending on whether video uploads are enabled
|
||||||
|
max_width = 128
|
||||||
|
|
||||||
|
# Optional: Maximum height in pixels for uploaded gifs
|
||||||
|
# environment variable: PICTRS__MEDIA__GIF__MAX_HEIGHT
|
||||||
|
# default: 128
|
||||||
|
#
|
||||||
|
# If a gif does not fit within this bound, it will either be transcoded to a video or rejected,
|
||||||
|
# depending on whether video uploads are enabled
|
||||||
|
max_height = 128
|
||||||
|
|
||||||
|
# Optional: Maximum area in pixels for uploaded gifs
|
||||||
|
# environment variable: PICTRS__MEDIA__GIF__MAX_AREA
|
||||||
|
# default: 16384 (128 * 128)
|
||||||
|
#
|
||||||
|
# If a gif does not fit within this bound, it will either be transcoded to a video or rejected,
|
||||||
|
# depending on whether video uploads are enabled
|
||||||
|
max_area = 16384
|
||||||
|
|
||||||
|
|
||||||
## Database configuration
|
## Database configuration
|
||||||
[repo]
|
[repo]
|
||||||
|
|
|
@ -11,7 +11,9 @@ use config::Config;
|
||||||
use defaults::Defaults;
|
use defaults::Defaults;
|
||||||
|
|
||||||
pub(crate) use commandline::Operation;
|
pub(crate) use commandline::Operation;
|
||||||
pub(crate) use file::{ConfigFile as Configuration, OpenTelemetry, Repo, Sled, Tracing};
|
pub(crate) use file::{
|
||||||
|
ConfigFile as Configuration, Media as MediaConfiguration, OpenTelemetry, Repo, Sled, Tracing,
|
||||||
|
};
|
||||||
pub(crate) use primitives::{
|
pub(crate) use primitives::{
|
||||||
AudioCodec, Filesystem, ImageFormat, LogFormat, ObjectStorage, Store, VideoCodec,
|
AudioCodec, Filesystem, ImageFormat, LogFormat, ObjectStorage, Store, VideoCodec,
|
||||||
};
|
};
|
||||||
|
|
|
@ -52,6 +52,9 @@ impl Args {
|
||||||
media_max_area,
|
media_max_area,
|
||||||
media_max_file_size,
|
media_max_file_size,
|
||||||
media_max_frame_count,
|
media_max_frame_count,
|
||||||
|
media_gif_max_width,
|
||||||
|
media_gif_max_height,
|
||||||
|
media_gif_max_area,
|
||||||
media_enable_silent_video,
|
media_enable_silent_video,
|
||||||
media_enable_full_video,
|
media_enable_full_video,
|
||||||
media_video_codec,
|
media_video_codec,
|
||||||
|
@ -66,6 +69,18 @@ impl Args {
|
||||||
api_key,
|
api_key,
|
||||||
worker_id,
|
worker_id,
|
||||||
};
|
};
|
||||||
|
let gif = if media_gif_max_width.is_none()
|
||||||
|
&& media_gif_max_height.is_none()
|
||||||
|
&& media_gif_max_area.is_none()
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Gif {
|
||||||
|
max_width: media_gif_max_width,
|
||||||
|
max_height: media_gif_max_height,
|
||||||
|
max_area: media_gif_max_area,
|
||||||
|
})
|
||||||
|
};
|
||||||
let media = Media {
|
let media = Media {
|
||||||
preprocess_steps: media_preprocess_steps,
|
preprocess_steps: media_preprocess_steps,
|
||||||
skip_validate_imports: media_skip_validate_imports,
|
skip_validate_imports: media_skip_validate_imports,
|
||||||
|
@ -74,6 +89,7 @@ impl Args {
|
||||||
max_area: media_max_area,
|
max_area: media_max_area,
|
||||||
max_file_size: media_max_file_size,
|
max_file_size: media_max_file_size,
|
||||||
max_frame_count: media_max_frame_count,
|
max_frame_count: media_max_frame_count,
|
||||||
|
gif,
|
||||||
enable_silent_video: media_enable_silent_video,
|
enable_silent_video: media_enable_silent_video,
|
||||||
enable_full_video: media_enable_full_video,
|
enable_full_video: media_enable_full_video,
|
||||||
video_codec: media_video_codec,
|
video_codec: media_video_codec,
|
||||||
|
@ -322,6 +338,8 @@ struct Media {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
max_frame_count: Option<usize>,
|
max_frame_count: Option<usize>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
gif: Option<Gif>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
enable_silent_video: Option<bool>,
|
enable_silent_video: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
enable_full_video: Option<bool>,
|
enable_full_video: Option<bool>,
|
||||||
|
@ -339,6 +357,17 @@ struct Media {
|
||||||
cache_duration: Option<i64>,
|
cache_duration: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
struct Gif {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
max_width: Option<usize>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
max_height: Option<usize>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
max_area: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the pict-rs application
|
/// Run the pict-rs application
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
|
@ -431,6 +460,24 @@ struct Run {
|
||||||
/// The maximum number of frames allowed for uploaded GIF and MP4s.
|
/// The maximum number of frames allowed for uploaded GIF and MP4s.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
media_max_frame_count: Option<usize>,
|
media_max_frame_count: Option<usize>,
|
||||||
|
/// Maximum width allowed for gif uploads.
|
||||||
|
///
|
||||||
|
/// If an upload exceeds this value, it will be transcoded to a video format or aborted,
|
||||||
|
/// depending on whether video uploads are enabled.
|
||||||
|
#[arg(long)]
|
||||||
|
media_gif_max_width: Option<usize>,
|
||||||
|
/// Maximum height allowed for gif uploads
|
||||||
|
///
|
||||||
|
/// If an upload exceeds this value, it will be transcoded to a video format or aborted,
|
||||||
|
/// depending on whether video uploads are enabled.
|
||||||
|
#[arg(long)]
|
||||||
|
media_gif_max_height: Option<usize>,
|
||||||
|
/// Maximum area allowed for gif uploads
|
||||||
|
///
|
||||||
|
/// If an upload exceeds this value, it will be transcoded to a video format or aborted,
|
||||||
|
/// depending on whether video uploads are enabled.
|
||||||
|
#[arg(long)]
|
||||||
|
media_gif_max_area: Option<usize>,
|
||||||
/// Whether to enable GIF and silent video uploads
|
/// Whether to enable GIF and silent video uploads
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
media_enable_silent_video: Option<bool>,
|
media_enable_silent_video: Option<bool>,
|
||||||
|
|
|
@ -66,6 +66,7 @@ struct MediaDefaults {
|
||||||
max_area: usize,
|
max_area: usize,
|
||||||
max_file_size: usize,
|
max_file_size: usize,
|
||||||
max_frame_count: usize,
|
max_frame_count: usize,
|
||||||
|
gif: GifDefaults,
|
||||||
enable_silent_video: bool,
|
enable_silent_video: bool,
|
||||||
enable_full_video: bool,
|
enable_full_video: bool,
|
||||||
video_codec: VideoCodec,
|
video_codec: VideoCodec,
|
||||||
|
@ -74,6 +75,14 @@ struct MediaDefaults {
|
||||||
cache_duration: i64,
|
cache_duration: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
struct GifDefaults {
|
||||||
|
max_height: usize,
|
||||||
|
max_width: usize,
|
||||||
|
max_area: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Serialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
|
@ -154,6 +163,7 @@ impl Default for MediaDefaults {
|
||||||
max_area: 40_000_000,
|
max_area: 40_000_000,
|
||||||
max_file_size: 40,
|
max_file_size: 40,
|
||||||
max_frame_count: 900,
|
max_frame_count: 900,
|
||||||
|
gif: Default::default(),
|
||||||
enable_silent_video: true,
|
enable_silent_video: true,
|
||||||
enable_full_video: false,
|
enable_full_video: false,
|
||||||
video_codec: VideoCodec::Vp9,
|
video_codec: VideoCodec::Vp9,
|
||||||
|
@ -171,6 +181,16 @@ impl Default for MediaDefaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for GifDefaults {
|
||||||
|
fn default() -> Self {
|
||||||
|
GifDefaults {
|
||||||
|
max_height: 128,
|
||||||
|
max_width: 128,
|
||||||
|
max_area: 16384,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for RepoDefaults {
|
impl Default for RepoDefaults {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Sled(SledDefaults::default())
|
Self::Sled(SledDefaults::default())
|
||||||
|
|
|
@ -100,12 +100,15 @@ pub(crate) struct Media {
|
||||||
|
|
||||||
pub(crate) max_frame_count: usize,
|
pub(crate) max_frame_count: usize,
|
||||||
|
|
||||||
|
pub(crate) gif: Gif,
|
||||||
|
|
||||||
pub(crate) enable_silent_video: bool,
|
pub(crate) enable_silent_video: bool,
|
||||||
|
|
||||||
pub(crate) enable_full_video: bool,
|
pub(crate) enable_full_video: bool,
|
||||||
|
|
||||||
pub(crate) video_codec: VideoCodec,
|
pub(crate) video_codec: VideoCodec,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub(crate) audio_codec: Option<AudioCodec>,
|
pub(crate) audio_codec: Option<AudioCodec>,
|
||||||
|
|
||||||
pub(crate) filters: BTreeSet<String>,
|
pub(crate) filters: BTreeSet<String>,
|
||||||
|
@ -118,6 +121,15 @@ pub(crate) struct Media {
|
||||||
pub(crate) cache_duration: i64,
|
pub(crate) cache_duration: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub(crate) struct Gif {
|
||||||
|
pub(crate) max_width: usize,
|
||||||
|
|
||||||
|
pub(crate) max_height: usize,
|
||||||
|
|
||||||
|
pub(crate) max_area: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl Media {
|
impl Media {
|
||||||
pub(crate) fn preprocess_steps(&self) -> Option<&[(String, String)]> {
|
pub(crate) fn preprocess_steps(&self) -> Option<&[(String, String)]> {
|
||||||
static PREPROCESS_STEPS: OnceCell<Vec<(String, String)>> = OnceCell::new();
|
static PREPROCESS_STEPS: OnceCell<Vec<(String, String)>> = OnceCell::new();
|
||||||
|
|
217
src/ffmpeg.rs
217
src/ffmpeg.rs
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{AudioCodec, ImageFormat, VideoCodec},
|
config::{AudioCodec, ImageFormat, MediaConfiguration, VideoCodec},
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
magick::{Details, ValidInputType},
|
magick::{Details, ValidInputType},
|
||||||
process::Process,
|
process::Process,
|
||||||
|
@ -8,6 +8,160 @@ use crate::{
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct TranscodeOptions {
|
||||||
|
input_format: VideoFormat,
|
||||||
|
output: TranscodeOutputOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum TranscodeOutputOptions {
|
||||||
|
Gif,
|
||||||
|
Video {
|
||||||
|
video_codec: VideoCodec,
|
||||||
|
audio_codec: Option<AudioCodec>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TranscodeOptions {
|
||||||
|
pub(crate) fn new(
|
||||||
|
media: &MediaConfiguration,
|
||||||
|
details: &Details,
|
||||||
|
input_format: VideoFormat,
|
||||||
|
) -> Self {
|
||||||
|
if let VideoFormat::Gif = input_format {
|
||||||
|
if details.width <= media.gif.max_width
|
||||||
|
&& details.height <= media.gif.max_height
|
||||||
|
&& details.width * details.height <= media.gif.max_area
|
||||||
|
{
|
||||||
|
return Self {
|
||||||
|
input_format,
|
||||||
|
output: TranscodeOutputOptions::gif(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
input_format,
|
||||||
|
output: TranscodeOutputOptions::video(media),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn input_file_extension(&self) -> &'static str {
|
||||||
|
self.input_format.to_file_extension()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn output_ffmpeg_video_codec(&self) -> &'static str {
|
||||||
|
match self.output {
|
||||||
|
TranscodeOutputOptions::Gif => "gif",
|
||||||
|
TranscodeOutputOptions::Video { video_codec, .. } => video_codec.to_ffmpeg_codec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn output_ffmpeg_audio_codec(&self) -> Option<&'static str> {
|
||||||
|
match self.output {
|
||||||
|
TranscodeOutputOptions::Video {
|
||||||
|
audio_codec: Some(audio_codec),
|
||||||
|
..
|
||||||
|
} => Some(audio_codec.to_ffmpeg_codec()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn output_ffmpeg_format(&self) -> &'static str {
|
||||||
|
match self.output {
|
||||||
|
TranscodeOutputOptions::Gif => "gif",
|
||||||
|
TranscodeOutputOptions::Video { video_codec, .. } => {
|
||||||
|
video_codec.to_output_format().to_ffmpeg_format()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn output_file_extension(&self) -> &'static str {
|
||||||
|
match self.output {
|
||||||
|
TranscodeOutputOptions::Gif => ".gif",
|
||||||
|
TranscodeOutputOptions::Video { video_codec, .. } => {
|
||||||
|
video_codec.to_output_format().to_file_extension()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute<'a>(
|
||||||
|
&self,
|
||||||
|
input_path: &str,
|
||||||
|
output_path: &'a str,
|
||||||
|
) -> Result<Process, std::io::Error> {
|
||||||
|
if let Some(audio_codec) = self.output_ffmpeg_audio_codec() {
|
||||||
|
Process::run(
|
||||||
|
"ffmpeg",
|
||||||
|
&[
|
||||||
|
"-i",
|
||||||
|
input_path,
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
"-vf",
|
||||||
|
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||||
|
"-c:a",
|
||||||
|
audio_codec,
|
||||||
|
"-c:v",
|
||||||
|
self.output_ffmpeg_video_codec(),
|
||||||
|
"-f",
|
||||||
|
self.output_ffmpeg_format(),
|
||||||
|
output_path,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Process::run(
|
||||||
|
"ffmpeg",
|
||||||
|
&[
|
||||||
|
"-i",
|
||||||
|
input_path,
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
"-vf",
|
||||||
|
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||||
|
"-an",
|
||||||
|
"-c:v",
|
||||||
|
self.output_ffmpeg_video_codec(),
|
||||||
|
"-f",
|
||||||
|
self.output_ffmpeg_format(),
|
||||||
|
output_path,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn output_type(&self) -> ValidInputType {
|
||||||
|
match self.output {
|
||||||
|
TranscodeOutputOptions::Gif => ValidInputType::Gif,
|
||||||
|
TranscodeOutputOptions::Video { video_codec, .. } => {
|
||||||
|
ValidInputType::from_video_codec(video_codec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TranscodeOutputOptions {
|
||||||
|
fn video(media: &MediaConfiguration) -> Self {
|
||||||
|
Self::Video {
|
||||||
|
video_codec: media.video_codec,
|
||||||
|
audio_codec: if media.enable_full_video {
|
||||||
|
Some(
|
||||||
|
media
|
||||||
|
.audio_codec
|
||||||
|
.unwrap_or(media.video_codec.to_output_format().default_audio_codec()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn gif() -> Self {
|
||||||
|
Self::Gif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) enum VideoFormat {
|
pub(crate) enum VideoFormat {
|
||||||
Gif,
|
Gif,
|
||||||
|
@ -145,9 +299,12 @@ const FORMAT_MAPPINGS: &[(&str, VideoFormat)] = &[
|
||||||
("webm", VideoFormat::Webm),
|
("webm", VideoFormat::Webm),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub(crate) async fn input_type_bytes(input: Bytes) -> Result<Option<ValidInputType>, Error> {
|
pub(crate) async fn input_type_bytes(
|
||||||
|
input: Bytes,
|
||||||
|
) -> Result<Option<(Details, ValidInputType)>, Error> {
|
||||||
if let Some(details) = details_bytes(input).await? {
|
if let Some(details) = details_bytes(input).await? {
|
||||||
return Ok(Some(details.validate_input()?));
|
let input_type = details.validate_input()?;
|
||||||
|
return Ok(Some((details, input_type)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
@ -264,19 +421,15 @@ fn parse_details_inner(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(input))]
|
#[tracing::instrument(skip(input))]
|
||||||
pub(crate) async fn trancsocde_bytes(
|
pub(crate) async fn transcode_bytes(
|
||||||
input: Bytes,
|
input: Bytes,
|
||||||
input_format: VideoFormat,
|
transcode_options: TranscodeOptions,
|
||||||
permit_audio: bool,
|
|
||||||
video_codec: VideoCodec,
|
|
||||||
audio_codec: Option<AudioCodec>,
|
|
||||||
) -> Result<impl AsyncRead + Unpin, Error> {
|
) -> Result<impl AsyncRead + Unpin, Error> {
|
||||||
let input_file = crate::tmp_file::tmp_file(Some(input_format.to_file_extension()));
|
let input_file = crate::tmp_file::tmp_file(Some(transcode_options.input_file_extension()));
|
||||||
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?;
|
||||||
|
|
||||||
let output_file =
|
let output_file = crate::tmp_file::tmp_file(Some(transcode_options.output_file_extension()));
|
||||||
crate::tmp_file::tmp_file(Some(video_codec.to_output_format().to_file_extension()));
|
|
||||||
let output_file_str = output_file.to_str().ok_or(UploadError::Path)?;
|
let output_file_str = output_file.to_str().ok_or(UploadError::Path)?;
|
||||||
crate::store::file_store::safe_create_parent(&output_file).await?;
|
crate::store::file_store::safe_create_parent(&output_file).await?;
|
||||||
|
|
||||||
|
@ -284,47 +437,7 @@ pub(crate) async fn trancsocde_bytes(
|
||||||
tmp_one.write_from_bytes(input).await?;
|
tmp_one.write_from_bytes(input).await?;
|
||||||
tmp_one.close().await?;
|
tmp_one.close().await?;
|
||||||
|
|
||||||
let output_format = video_codec.to_output_format();
|
let process = transcode_options.execute(input_file_str, output_file_str)?;
|
||||||
let audio_codec = audio_codec.unwrap_or_else(|| output_format.default_audio_codec());
|
|
||||||
|
|
||||||
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",
|
|
||||||
audio_codec.to_ffmpeg_codec(),
|
|
||||||
"-c:v",
|
|
||||||
video_codec.to_ffmpeg_codec(),
|
|
||||||
"-f",
|
|
||||||
output_format.to_ffmpeg_format(),
|
|
||||||
output_file_str,
|
|
||||||
],
|
|
||||||
)?
|
|
||||||
} else {
|
|
||||||
Process::run(
|
|
||||||
"ffmpeg",
|
|
||||||
&[
|
|
||||||
"-i",
|
|
||||||
input_file_str,
|
|
||||||
"-pix_fmt",
|
|
||||||
"yuv420p",
|
|
||||||
"-vf",
|
|
||||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
||||||
"-an",
|
|
||||||
"-c:v",
|
|
||||||
video_codec.to_ffmpeg_codec(),
|
|
||||||
"-f",
|
|
||||||
output_format.to_ffmpeg_format(),
|
|
||||||
output_file_str,
|
|
||||||
],
|
|
||||||
)?
|
|
||||||
};
|
|
||||||
|
|
||||||
process.wait().await?;
|
process.wait().await?;
|
||||||
tokio::fs::remove_file(input_file).await?;
|
tokio::fs::remove_file(input_file).await?;
|
||||||
|
|
|
@ -59,16 +59,8 @@ where
|
||||||
let bytes = aggregate(stream).await?;
|
let bytes = aggregate(stream).await?;
|
||||||
|
|
||||||
tracing::trace!("Validating bytes");
|
tracing::trace!("Validating bytes");
|
||||||
let (input_type, validated_reader) = crate::validate::validate_bytes(
|
let (input_type, validated_reader) =
|
||||||
bytes,
|
crate::validate::validate_bytes(bytes, &CONFIG.media, should_validate).await?;
|
||||||
CONFIG.media.format,
|
|
||||||
CONFIG.media.enable_silent_video,
|
|
||||||
CONFIG.media.enable_full_video,
|
|
||||||
CONFIG.media.video_codec,
|
|
||||||
CONFIG.media.audio_codec,
|
|
||||||
should_validate,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let processed_reader = if let Some(operations) = CONFIG.media.preprocess_steps() {
|
let processed_reader = if let Some(operations) = CONFIG.media.preprocess_steps() {
|
||||||
if let Some(format) = input_type.to_format() {
|
if let Some(format) = input_type.to_format() {
|
||||||
|
|
|
@ -45,7 +45,7 @@ pub(crate) enum ValidInputType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValidInputType {
|
impl ValidInputType {
|
||||||
fn as_str(self) -> &'static str {
|
const fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 => "MP4",
|
Self::Mp4 => "MP4",
|
||||||
Self::Webm => "WEBM",
|
Self::Webm => "WEBM",
|
||||||
|
@ -56,7 +56,7 @@ impl ValidInputType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn as_ext(self) -> &'static str {
|
pub(crate) const fn as_ext(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 => ".mp4",
|
Self::Mp4 => ".mp4",
|
||||||
Self::Webm => ".webm",
|
Self::Webm => ".webm",
|
||||||
|
@ -67,11 +67,11 @@ impl ValidInputType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_video(self) -> bool {
|
pub(crate) const fn is_video(self) -> bool {
|
||||||
matches!(self, Self::Mp4 | Self::Webm | Self::Gif)
|
matches!(self, Self::Mp4 | Self::Webm | Self::Gif)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn video_hint(self) -> Option<&'static str> {
|
const fn video_hint(self) -> Option<&'static str> {
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 => Some(".mp4"),
|
Self::Mp4 => Some(".mp4"),
|
||||||
Self::Webm => Some(".webm"),
|
Self::Webm => Some(".webm"),
|
||||||
|
@ -80,14 +80,14 @@ impl ValidInputType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_video_codec(codec: VideoCodec) -> Self {
|
pub(crate) const fn from_video_codec(codec: VideoCodec) -> Self {
|
||||||
match codec {
|
match codec {
|
||||||
VideoCodec::Av1 | VideoCodec::Vp8 | VideoCodec::Vp9 => Self::Webm,
|
VideoCodec::Av1 | VideoCodec::Vp8 | VideoCodec::Vp9 => Self::Webm,
|
||||||
VideoCodec::H264 | VideoCodec::H265 => Self::Mp4,
|
VideoCodec::H264 | VideoCodec::H265 => Self::Mp4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_format(format: ImageFormat) -> Self {
|
pub(crate) const fn from_format(format: ImageFormat) -> Self {
|
||||||
match format {
|
match format {
|
||||||
ImageFormat::Jpeg => ValidInputType::Jpeg,
|
ImageFormat::Jpeg => ValidInputType::Jpeg,
|
||||||
ImageFormat::Png => ValidInputType::Png,
|
ImageFormat::Png => ValidInputType::Png,
|
||||||
|
@ -95,7 +95,7 @@ impl ValidInputType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_format(self) -> Option<ImageFormat> {
|
pub(crate) const fn to_format(self) -> Option<ImageFormat> {
|
||||||
match self {
|
match self {
|
||||||
Self::Jpeg => Some(ImageFormat::Jpeg),
|
Self::Jpeg => Some(ImageFormat::Jpeg),
|
||||||
Self::Png => Some(ImageFormat::Png),
|
Self::Png => Some(ImageFormat::Png),
|
||||||
|
@ -283,8 +283,10 @@ fn parse_details(s: std::borrow::Cow<'_, str>) -> Result<Details, Error> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn input_type_bytes(input: Bytes) -> Result<ValidInputType, Error> {
|
pub(crate) async fn input_type_bytes(input: Bytes) -> Result<(Details, ValidInputType), Error> {
|
||||||
details_bytes(input, None).await?.validate_input()
|
let details = details_bytes(input, None).await?;
|
||||||
|
let input_type = details.validate_input()?;
|
||||||
|
Ok((details, input_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_image(args: Vec<String>, format: ImageFormat) -> std::io::Result<Process> {
|
fn process_image(args: Vec<String>, format: ImageFormat) -> std::io::Result<Process> {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{AudioCodec, ImageFormat, VideoCodec},
|
config::{ImageFormat, MediaConfiguration},
|
||||||
either::Either,
|
either::Either,
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
ffmpeg::FileFormat,
|
ffmpeg::{FileFormat, TranscodeOptions},
|
||||||
magick::ValidInputType,
|
magick::ValidInputType,
|
||||||
};
|
};
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
|
@ -38,16 +38,12 @@ impl AsyncRead for UnvalidatedBytes {
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub(crate) async fn validate_bytes(
|
pub(crate) async fn validate_bytes(
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
prescribed_format: Option<ImageFormat>,
|
media: &MediaConfiguration,
|
||||||
enable_silent_video: bool,
|
|
||||||
enable_full_video: bool,
|
|
||||||
video_codec: VideoCodec,
|
|
||||||
audio_codec: Option<AudioCodec>,
|
|
||||||
validate: bool,
|
validate: bool,
|
||||||
) -> Result<(ValidInputType, impl AsyncRead + Unpin), Error> {
|
) -> Result<(ValidInputType, impl AsyncRead + Unpin), Error> {
|
||||||
let input_type =
|
let (details, input_type) =
|
||||||
if let Some(input_type) = crate::ffmpeg::input_type_bytes(bytes.clone()).await? {
|
if let Some(tup) = crate::ffmpeg::input_type_bytes(bytes.clone()).await? {
|
||||||
input_type
|
tup
|
||||||
} else {
|
} else {
|
||||||
crate::magick::input_type_bytes(bytes.clone()).await?
|
crate::magick::input_type_bytes(bytes.clone()).await?
|
||||||
};
|
};
|
||||||
|
@ -56,22 +52,17 @@ pub(crate) async fn validate_bytes(
|
||||||
return Ok((input_type, Either::left(UnvalidatedBytes::new(bytes))));
|
return Ok((input_type, Either::left(UnvalidatedBytes::new(bytes))));
|
||||||
}
|
}
|
||||||
|
|
||||||
match (input_type.to_file_format(), prescribed_format) {
|
match (input_type.to_file_format(), media.format) {
|
||||||
(FileFormat::Video(video_format), _) => {
|
(FileFormat::Video(video_format), _) => {
|
||||||
if !(enable_silent_video || enable_full_video) {
|
if !(media.enable_silent_video || media.enable_full_video) {
|
||||||
return Err(UploadError::SilentVideoDisabled.into());
|
return Err(UploadError::SilentVideoDisabled.into());
|
||||||
}
|
}
|
||||||
|
let transcode_options = TranscodeOptions::new(media, &details, video_format);
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
ValidInputType::from_video_codec(video_codec),
|
transcode_options.output_type(),
|
||||||
Either::right(Either::left(Either::left(
|
Either::right(Either::left(Either::left(
|
||||||
crate::ffmpeg::trancsocde_bytes(
|
crate::ffmpeg::transcode_bytes(bytes, transcode_options).await?,
|
||||||
bytes,
|
|
||||||
video_format,
|
|
||||||
enable_full_video,
|
|
||||||
video_codec,
|
|
||||||
audio_codec,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
))),
|
))),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue