mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 19:31:35 +00:00
Enable selecting video and audio codecs for uploaded media
This commit is contained in:
parent
f4542efcc1
commit
8eb2293808
15 changed files with 252 additions and 45 deletions
|
@ -80,9 +80,13 @@ Options:
|
||||||
--media-max-frame-count <MEDIA_MAX_FRAME_COUNT>
|
--media-max-frame-count <MEDIA_MAX_FRAME_COUNT>
|
||||||
The maximum number of frames allowed for uploaded GIF and MP4s
|
The maximum number of frames allowed for uploaded GIF and MP4s
|
||||||
--media-enable-silent-video <MEDIA_ENABLE_SILENT_VIDEO>
|
--media-enable-silent-video <MEDIA_ENABLE_SILENT_VIDEO>
|
||||||
Whether to enable GIF and silent MP4 uploads [possible values: true, false]
|
Whether to enable GIF and silent video uploads [possible values: true, false]
|
||||||
--media-enable-full-video <MEDIA_ENABLE_FULL_VIDEO>
|
--media-enable-full-video <MEDIA_ENABLE_FULL_VIDEO>
|
||||||
Whether to enable full MP4 uploads [possible values: true, false]
|
Whether to enable full video uploads [possible values: true, false]
|
||||||
|
--media-video-codec <MEDIA_VIDEO_CODEC>
|
||||||
|
Enforce a specific video codec for uploaded videos [possible values: h264, h265, av1, vp8, vp9]
|
||||||
|
--media-audio-codec <MEDIA_AUDIO_CODEC>
|
||||||
|
Enforce a specific audio codec for uploaded videos [possible values: aac, opus, vorbis]
|
||||||
--media-filters <MEDIA_FILTERS>
|
--media-filters <MEDIA_FILTERS>
|
||||||
Which media filters should be enabled on the `process` endpoint
|
Which media filters should be enabled on the `process` endpoint
|
||||||
--media-format <MEDIA_FORMAT>
|
--media-format <MEDIA_FORMAT>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[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'
|
||||||
|
@ -23,6 +24,7 @@ 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 = 'h264'
|
||||||
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
|
||||||
|
|
20
pict-rs.toml
20
pict-rs.toml
|
@ -159,6 +159,26 @@ enable_silent_video = true
|
||||||
# default: false
|
# default: false
|
||||||
enable_full_video = false
|
enable_full_video = false
|
||||||
|
|
||||||
|
## Optional: set the default video codec
|
||||||
|
# environment variable: PICTRS__MEDIA__VIDEO_CODEC
|
||||||
|
# default: h264
|
||||||
|
#
|
||||||
|
# available options: av1, h264, h265, vp8, vp9
|
||||||
|
# this setting does nothing if video is not enabled
|
||||||
|
video_codec = "h264"
|
||||||
|
|
||||||
|
## Optional: set the default audio codec
|
||||||
|
# environment variable: PICTRS__MEDIA__AUDIO_CODEC
|
||||||
|
# default: empty
|
||||||
|
#
|
||||||
|
# available options: aac, opus, vorbis
|
||||||
|
# The audio codec is automatically selected based on video codec, but can be overriden
|
||||||
|
# av1, vp8, and vp9 map to opus
|
||||||
|
# h264 and h265 map to aac
|
||||||
|
# vorbis is not default for any codec
|
||||||
|
# this setting does nothing if full video is not enabled
|
||||||
|
audio_codec = "aac"
|
||||||
|
|
||||||
## 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']
|
||||||
|
|
|
@ -11,7 +11,9 @@ 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, OpenTelemetry, Repo, Sled, Tracing};
|
||||||
pub(crate) use primitives::{Filesystem, ImageFormat, LogFormat, ObjectStorage, Store};
|
pub(crate) use primitives::{
|
||||||
|
AudioCodec, Filesystem, ImageFormat, LogFormat, ObjectStorage, Store, VideoCodec,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) fn configure() -> color_eyre::Result<(Configuration, Operation)> {
|
pub(crate) fn configure() -> color_eyre::Result<(Configuration, Operation)> {
|
||||||
let Output {
|
let Output {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::primitives::{ImageFormat, LogFormat, Targets},
|
config::primitives::{AudioCodec, ImageFormat, LogFormat, Targets, VideoCodec},
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
};
|
};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
@ -54,6 +54,8 @@ impl Args {
|
||||||
media_max_frame_count,
|
media_max_frame_count,
|
||||||
media_enable_silent_video,
|
media_enable_silent_video,
|
||||||
media_enable_full_video,
|
media_enable_full_video,
|
||||||
|
media_video_codec,
|
||||||
|
media_audio_codec,
|
||||||
media_filters,
|
media_filters,
|
||||||
media_format,
|
media_format,
|
||||||
media_cache_duration,
|
media_cache_duration,
|
||||||
|
@ -74,6 +76,8 @@ impl Args {
|
||||||
max_frame_count: media_max_frame_count,
|
max_frame_count: media_max_frame_count,
|
||||||
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,
|
||||||
|
audio_codec: media_audio_codec,
|
||||||
filters: media_filters,
|
filters: media_filters,
|
||||||
format: media_format,
|
format: media_format,
|
||||||
cache_duration: media_cache_duration,
|
cache_duration: media_cache_duration,
|
||||||
|
@ -322,6 +326,10 @@ struct Media {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
enable_full_video: Option<bool>,
|
enable_full_video: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
video_codec: Option<VideoCodec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
audio_codec: Option<AudioCodec>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
filters: Option<Vec<String>>,
|
filters: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
format: Option<ImageFormat>,
|
format: Option<ImageFormat>,
|
||||||
|
@ -423,12 +431,18 @@ 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>,
|
||||||
/// Whether to enable GIF and silent MP4 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>,
|
||||||
/// Whether to enable full MP4 uploads
|
/// Whether to enable full video uploads
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
media_enable_full_video: Option<bool>,
|
media_enable_full_video: Option<bool>,
|
||||||
|
/// Enforce a specific video codec for uploaded videos
|
||||||
|
#[arg(long)]
|
||||||
|
media_video_codec: Option<VideoCodec>,
|
||||||
|
/// Enforce a specific audio codec for uploaded videos
|
||||||
|
#[arg(long)]
|
||||||
|
media_audio_codec: Option<AudioCodec>,
|
||||||
/// Which media filters should be enabled on the `process` endpoint
|
/// Which media filters should be enabled on the `process` endpoint
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
media_filters: Option<Vec<String>>,
|
media_filters: Option<Vec<String>>,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::primitives::{LogFormat, Targets},
|
config::primitives::{LogFormat, Targets, VideoCodec},
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
};
|
};
|
||||||
use std::{net::SocketAddr, path::PathBuf};
|
use std::{net::SocketAddr, path::PathBuf};
|
||||||
|
@ -68,6 +68,7 @@ struct MediaDefaults {
|
||||||
max_frame_count: usize,
|
max_frame_count: usize,
|
||||||
enable_silent_video: bool,
|
enable_silent_video: bool,
|
||||||
enable_full_video: bool,
|
enable_full_video: bool,
|
||||||
|
video_codec: VideoCodec,
|
||||||
filters: Vec<String>,
|
filters: Vec<String>,
|
||||||
skip_validate_imports: bool,
|
skip_validate_imports: bool,
|
||||||
cache_duration: i64,
|
cache_duration: i64,
|
||||||
|
@ -155,6 +156,7 @@ impl Default for MediaDefaults {
|
||||||
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: VideoCodec::H264,
|
||||||
filters: vec![
|
filters: vec![
|
||||||
"blur".into(),
|
"blur".into(),
|
||||||
"crop".into(),
|
"crop".into(),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::primitives::{ImageFormat, LogFormat, Store, Targets},
|
config::primitives::{AudioCodec, ImageFormat, LogFormat, Store, Targets, VideoCodec},
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
};
|
};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
@ -104,6 +104,10 @@ pub(crate) struct Media {
|
||||||
|
|
||||||
pub(crate) enable_full_video: bool,
|
pub(crate) enable_full_video: bool,
|
||||||
|
|
||||||
|
pub(crate) video_codec: VideoCodec,
|
||||||
|
|
||||||
|
pub(crate) audio_codec: Option<AudioCodec>,
|
||||||
|
|
||||||
pub(crate) filters: BTreeSet<String>,
|
pub(crate) filters: BTreeSet<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|
|
@ -45,6 +45,48 @@ pub(crate) enum ImageFormat {
|
||||||
Png,
|
Png,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
Hash,
|
||||||
|
serde::Deserialize,
|
||||||
|
serde::Serialize,
|
||||||
|
ValueEnum,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub(crate) enum VideoCodec {
|
||||||
|
H264,
|
||||||
|
H265,
|
||||||
|
Av1,
|
||||||
|
Vp8,
|
||||||
|
Vp9,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
PartialOrd,
|
||||||
|
Ord,
|
||||||
|
Hash,
|
||||||
|
serde::Deserialize,
|
||||||
|
serde::Serialize,
|
||||||
|
ValueEnum,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub(crate) enum AudioCodec {
|
||||||
|
Aac,
|
||||||
|
Opus,
|
||||||
|
Vorbis,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct Targets {
|
pub(crate) struct Targets {
|
||||||
pub(crate) targets: tracing_subscriber::filter::Targets,
|
pub(crate) targets: tracing_subscriber::filter::Targets,
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
use crate::{error::Error, magick::ValidInputType, serde_str::Serde, store::Store};
|
use crate::{
|
||||||
|
error::Error,
|
||||||
|
ffmpeg::InputFormat,
|
||||||
|
magick::{video_mp4, video_webm, ValidInputType},
|
||||||
|
serde_str::Serde,
|
||||||
|
store::Store,
|
||||||
|
};
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
@ -93,6 +99,22 @@ impl Details {
|
||||||
pub(crate) fn system_time(&self) -> std::time::SystemTime {
|
pub(crate) fn system_time(&self) -> std::time::SystemTime {
|
||||||
self.created_at.into()
|
self.created_at.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_input_format(&self) -> Option<InputFormat> {
|
||||||
|
if *self.content_type == mime::IMAGE_GIF {
|
||||||
|
return Some(InputFormat::Gif);
|
||||||
|
}
|
||||||
|
|
||||||
|
if *self.content_type == video_mp4() {
|
||||||
|
return Some(InputFormat::Mp4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if *self.content_type == video_webm() {
|
||||||
|
return Some(InputFormat::Webm);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MaybeHumanDate> for std::time::SystemTime {
|
impl From<MaybeHumanDate> for std::time::SystemTime {
|
||||||
|
|
104
src/ffmpeg.rs
104
src/ffmpeg.rs
|
@ -1,4 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::{AudioCodec, VideoCodec},
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
magick::{Details, ValidInputType},
|
magick::{Details, ValidInputType},
|
||||||
process::Process,
|
process::Process,
|
||||||
|
@ -8,25 +9,31 @@ use actix_web::web::Bytes;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) enum InputFormat {
|
pub(crate) enum InputFormat {
|
||||||
Gif,
|
Gif,
|
||||||
Mp4,
|
Mp4,
|
||||||
Webm,
|
Webm,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) enum OutputFormat {
|
||||||
|
Mp4,
|
||||||
|
Webm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) enum ThumbnailFormat {
|
pub(crate) enum ThumbnailFormat {
|
||||||
Jpeg,
|
Jpeg,
|
||||||
// Webp,
|
// Webp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputFormat {
|
impl InputFormat {
|
||||||
fn to_ext(self) -> &'static str {
|
const fn to_ext(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
InputFormat::Gif => ".gif",
|
Self::Gif => ".gif",
|
||||||
InputFormat::Mp4 => ".mp4",
|
Self::Mp4 => ".mp4",
|
||||||
InputFormat::Webm => ".webm",
|
Self::Webm => ".webm",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,23 +47,69 @@ impl InputFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThumbnailFormat {
|
impl ThumbnailFormat {
|
||||||
fn as_codec(self) -> &'static str {
|
const fn as_ffmpeg_codec(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ThumbnailFormat::Jpeg => "mjpeg",
|
Self::Jpeg => "mjpeg",
|
||||||
// ThumbnailFormat::Webp => "webp",
|
// Self::Webp => "webp",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_ext(self) -> &'static str {
|
const fn to_ext(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ThumbnailFormat::Jpeg => ".jpeg",
|
Self::Jpeg => ".jpeg",
|
||||||
|
// Self::Webp => ".webp",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_format(self) -> &'static str {
|
const fn as_ffmpeg_format(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
ThumbnailFormat::Jpeg => "image2",
|
Self::Jpeg => "image2",
|
||||||
// ThumbnailFormat::Webp => "webp",
|
// Self::Webp => "webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputFormat {
|
||||||
|
const fn to_ffmpeg_format(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 => "mp4",
|
||||||
|
Self::Webm => "webm",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_audio_codec(self) -> AudioCodec {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 => AudioCodec::Aac,
|
||||||
|
Self::Webm => AudioCodec::Opus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoCodec {
|
||||||
|
const fn to_output_format(self) -> OutputFormat {
|
||||||
|
match self {
|
||||||
|
Self::H264 | Self::H265 => OutputFormat::Mp4,
|
||||||
|
Self::Av1 | Self::Vp8 | Self::Vp9 => OutputFormat::Webm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn to_ffmpeg_codec(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Av1 => "av1",
|
||||||
|
Self::H264 => "h264",
|
||||||
|
Self::H265 => "hevc",
|
||||||
|
Self::Vp8 => "vp8",
|
||||||
|
Self::Vp9 => "vp9",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioCodec {
|
||||||
|
const fn to_ffmpeg_codec(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Aac => "aac",
|
||||||
|
Self::Opus => "opus",
|
||||||
|
Self::Vorbis => "vorbis",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,11 +237,13 @@ fn parse_details_inner(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(name = "Convert to Mp4", skip(input))]
|
#[tracing::instrument(name = "Transcode video", skip(input))]
|
||||||
pub(crate) async fn to_mp4_bytes(
|
pub(crate) async fn trancsocde_bytes(
|
||||||
input: Bytes,
|
input: Bytes,
|
||||||
input_format: InputFormat,
|
input_format: InputFormat,
|
||||||
permit_audio: bool,
|
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_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)?;
|
||||||
|
@ -202,6 +257,9 @@ 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 output_format = video_codec.to_output_format();
|
||||||
|
let audio_codec = audio_codec.unwrap_or(output_format.default_audio_codec());
|
||||||
|
|
||||||
let process = if permit_audio {
|
let process = if permit_audio {
|
||||||
Process::run(
|
Process::run(
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
|
@ -213,11 +271,11 @@ 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",
|
||||||
"-c:a",
|
"-c:a",
|
||||||
"aac",
|
audio_codec.to_ffmpeg_codec(),
|
||||||
"-c:v",
|
"-c:v",
|
||||||
"h264",
|
video_codec.to_ffmpeg_codec(),
|
||||||
"-f",
|
"-f",
|
||||||
"mp4",
|
output_format.to_ffmpeg_format(),
|
||||||
output_file_str,
|
output_file_str,
|
||||||
],
|
],
|
||||||
)?
|
)?
|
||||||
|
@ -233,9 +291,9 @@ pub(crate) async fn to_mp4_bytes(
|
||||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||||
"-an",
|
"-an",
|
||||||
"-c:v",
|
"-c:v",
|
||||||
"h264",
|
video_codec.to_ffmpeg_codec(),
|
||||||
"-f",
|
"-f",
|
||||||
"mp4",
|
output_format.to_ffmpeg_format(),
|
||||||
output_file_str,
|
output_file_str,
|
||||||
],
|
],
|
||||||
)?
|
)?
|
||||||
|
@ -281,9 +339,9 @@ pub(crate) async fn thumbnail<S: Store>(
|
||||||
"-vframes",
|
"-vframes",
|
||||||
"1",
|
"1",
|
||||||
"-codec",
|
"-codec",
|
||||||
format.as_codec(),
|
format.as_ffmpeg_codec(),
|
||||||
"-f",
|
"-f",
|
||||||
format.as_format(),
|
format.as_ffmpeg_format(),
|
||||||
output_file_str,
|
output_file_str,
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -20,6 +20,8 @@ pub(crate) async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
alias: Alias,
|
alias: Alias,
|
||||||
thumbnail_path: PathBuf,
|
thumbnail_path: PathBuf,
|
||||||
thumbnail_args: Vec<String>,
|
thumbnail_args: Vec<String>,
|
||||||
|
input_format: Option<InputFormat>,
|
||||||
|
thumbnail_format: Option<ThumbnailFormat>,
|
||||||
hash: R::Bytes,
|
hash: R::Bytes,
|
||||||
) -> Result<(Details, Bytes), Error> {
|
) -> Result<(Details, Bytes), Error> {
|
||||||
let process_fut = process(
|
let process_fut = process(
|
||||||
|
@ -29,6 +31,8 @@ pub(crate) async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
alias,
|
alias,
|
||||||
thumbnail_path.clone(),
|
thumbnail_path.clone(),
|
||||||
thumbnail_args,
|
thumbnail_args,
|
||||||
|
input_format,
|
||||||
|
thumbnail_format,
|
||||||
hash.clone(),
|
hash.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -46,6 +50,8 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
alias: Alias,
|
alias: Alias,
|
||||||
thumbnail_path: PathBuf,
|
thumbnail_path: PathBuf,
|
||||||
thumbnail_args: Vec<String>,
|
thumbnail_args: Vec<String>,
|
||||||
|
input_format: Option<InputFormat>,
|
||||||
|
thumbnail_format: Option<ThumbnailFormat>,
|
||||||
hash: R::Bytes,
|
hash: R::Bytes,
|
||||||
) -> Result<(Details, Bytes), Error> {
|
) -> Result<(Details, Bytes), Error> {
|
||||||
let permit = crate::PROCESS_SEMAPHORE.acquire().await;
|
let permit = crate::PROCESS_SEMAPHORE.acquire().await;
|
||||||
|
@ -60,8 +66,8 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
let reader = crate::ffmpeg::thumbnail(
|
let reader = crate::ffmpeg::thumbnail(
|
||||||
store.clone(),
|
store.clone(),
|
||||||
identifier,
|
identifier,
|
||||||
InputFormat::Mp4,
|
input_format.unwrap_or(InputFormat::Mp4),
|
||||||
ThumbnailFormat::Jpeg,
|
thumbnail_format.unwrap_or(ThumbnailFormat::Jpeg),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let motion_identifier = store.save_async_read(reader).await?;
|
let motion_identifier = store.save_async_read(reader).await?;
|
||||||
|
|
|
@ -70,11 +70,13 @@ where
|
||||||
let bytes = aggregate(stream).await?;
|
let bytes = aggregate(stream).await?;
|
||||||
|
|
||||||
tracing::debug!("Validating bytes");
|
tracing::debug!("Validating bytes");
|
||||||
let (input_type, validated_reader) = crate::validate::validate_image_bytes(
|
let (input_type, validated_reader) = crate::validate::validate_bytes(
|
||||||
bytes,
|
bytes,
|
||||||
CONFIG.media.format,
|
CONFIG.media.format,
|
||||||
CONFIG.media.enable_silent_video,
|
CONFIG.media.enable_silent_video,
|
||||||
CONFIG.media.enable_full_video,
|
CONFIG.media.enable_full_video,
|
||||||
|
CONFIG.media.video_codec,
|
||||||
|
CONFIG.media.audio_codec,
|
||||||
should_validate,
|
should_validate,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -48,8 +48,6 @@ mod stream;
|
||||||
mod tmp_file;
|
mod tmp_file;
|
||||||
mod validate;
|
mod validate;
|
||||||
|
|
||||||
use crate::magick::ValidInputType;
|
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
backgrounded::Backgrounded,
|
backgrounded::Backgrounded,
|
||||||
config::{Configuration, ImageFormat, Operation},
|
config::{Configuration, ImageFormat, Operation},
|
||||||
|
@ -58,7 +56,7 @@ use self::{
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
ingest::Session,
|
ingest::Session,
|
||||||
init_tracing::init_tracing,
|
init_tracing::init_tracing,
|
||||||
magick::details_hint,
|
magick::{details_hint, ValidInputType},
|
||||||
middleware::{Deadline, Internal},
|
middleware::{Deadline, Internal},
|
||||||
queue::queue_generate,
|
queue::queue_generate,
|
||||||
repo::{
|
repo::{
|
||||||
|
@ -597,6 +595,7 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
|
|
||||||
let path_string = thumbnail_path.to_string_lossy().to_string();
|
let path_string = thumbnail_path.to_string_lossy().to_string();
|
||||||
let hash = repo.hash(&alias).await?;
|
let hash = repo.hash(&alias).await?;
|
||||||
|
|
||||||
let identifier_opt = repo
|
let identifier_opt = repo
|
||||||
.variant_identifier::<S::Identifier>(hash.clone(), path_string)
|
.variant_identifier::<S::Identifier>(hash.clone(), path_string)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -624,6 +623,8 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
return ranged_file_resp(&store, identifier, range, details).await;
|
return ranged_file_resp(&store, identifier, range, details).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let original_details = ensure_details(&repo, &store, &alias).await?;
|
||||||
|
|
||||||
let (details, bytes) = generate::generate(
|
let (details, bytes) = generate::generate(
|
||||||
&repo,
|
&repo,
|
||||||
&store,
|
&store,
|
||||||
|
@ -631,6 +632,8 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
alias,
|
alias,
|
||||||
thumbnail_path,
|
thumbnail_path,
|
||||||
thumbnail_args,
|
thumbnail_args,
|
||||||
|
original_details.to_input_format(),
|
||||||
|
None,
|
||||||
hash,
|
hash,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -149,6 +149,8 @@ async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let original_details = crate::ensure_details(repo, store, &source).await?;
|
||||||
|
|
||||||
crate::generate::generate(
|
crate::generate::generate(
|
||||||
repo,
|
repo,
|
||||||
store,
|
store,
|
||||||
|
@ -156,6 +158,8 @@ async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
source,
|
source,
|
||||||
process_path,
|
process_path,
|
||||||
process_args,
|
process_args,
|
||||||
|
original_details.to_input_format(),
|
||||||
|
None,
|
||||||
hash,
|
hash,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::ImageFormat,
|
config::{AudioCodec, ImageFormat, VideoCodec},
|
||||||
either::Either,
|
either::Either,
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
ffmpeg::InputFormat,
|
ffmpeg::InputFormat,
|
||||||
|
@ -36,12 +36,14 @@ impl AsyncRead for UnvalidatedBytes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(name = "Validate image", skip(bytes))]
|
#[instrument(name = "Validate media", skip(bytes))]
|
||||||
pub(crate) async fn validate_image_bytes(
|
pub(crate) async fn validate_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,
|
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 input_type =
|
||||||
|
@ -63,7 +65,14 @@ pub(crate) async fn validate_image_bytes(
|
||||||
Ok((
|
Ok((
|
||||||
ValidInputType::Mp4,
|
ValidInputType::Mp4,
|
||||||
Either::right(Either::left(
|
Either::right(Either::left(
|
||||||
crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Gif, false).await?,
|
crate::ffmpeg::trancsocde_bytes(
|
||||||
|
bytes,
|
||||||
|
InputFormat::Gif,
|
||||||
|
false,
|
||||||
|
video_codec,
|
||||||
|
audio_codec,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -74,7 +83,14 @@ pub(crate) async fn validate_image_bytes(
|
||||||
Ok((
|
Ok((
|
||||||
ValidInputType::Mp4,
|
ValidInputType::Mp4,
|
||||||
Either::right(Either::left(
|
Either::right(Either::left(
|
||||||
crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Mp4, enable_full_video).await?,
|
crate::ffmpeg::trancsocde_bytes(
|
||||||
|
bytes,
|
||||||
|
InputFormat::Mp4,
|
||||||
|
enable_full_video,
|
||||||
|
video_codec,
|
||||||
|
audio_codec,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -85,8 +101,14 @@ pub(crate) async fn validate_image_bytes(
|
||||||
Ok((
|
Ok((
|
||||||
ValidInputType::Mp4,
|
ValidInputType::Mp4,
|
||||||
Either::right(Either::left(
|
Either::right(Either::left(
|
||||||
crate::ffmpeg::to_mp4_bytes(bytes, InputFormat::Webm, enable_full_video)
|
crate::ffmpeg::trancsocde_bytes(
|
||||||
.await?,
|
bytes,
|
||||||
|
InputFormat::Webm,
|
||||||
|
enable_full_video,
|
||||||
|
video_codec,
|
||||||
|
audio_codec,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue