2021-10-23 19:14:12 +00:00
|
|
|
use crate::{
|
2022-10-02 02:17:18 +00:00
|
|
|
config::{AudioCodec, ImageFormat, VideoCodec},
|
2021-10-23 19:14:12 +00:00
|
|
|
error::{Error, UploadError},
|
2022-09-26 01:39:09 +00:00
|
|
|
magick::{Details, ValidInputType},
|
2021-10-23 19:14:12 +00:00
|
|
|
process::Process,
|
|
|
|
store::Store,
|
|
|
|
};
|
2021-09-04 00:53:53 +00:00
|
|
|
use actix_web::web::Bytes;
|
2022-09-26 00:34:51 +00:00
|
|
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
2021-08-26 02:46:11 +00:00
|
|
|
|
2022-10-01 00:38:11 +00:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
2022-10-02 02:17:18 +00:00
|
|
|
pub(crate) enum VideoFormat {
|
2021-08-31 02:19:47 +00:00
|
|
|
Gif,
|
|
|
|
Mp4,
|
2022-09-26 00:34:51 +00:00
|
|
|
Webm,
|
2021-08-31 02:19:47 +00:00
|
|
|
}
|
|
|
|
|
2022-10-01 00:38:11 +00:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
pub(crate) enum OutputFormat {
|
|
|
|
Mp4,
|
|
|
|
Webm,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
2021-08-28 22:15:14 +00:00
|
|
|
pub(crate) enum ThumbnailFormat {
|
|
|
|
Jpeg,
|
2021-08-31 02:19:47 +00:00
|
|
|
// Webp,
|
|
|
|
}
|
|
|
|
|
2022-10-02 02:17:18 +00:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
pub(crate) enum FileFormat {
|
|
|
|
Image(ImageFormat),
|
|
|
|
Video(VideoFormat),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ValidInputType {
|
|
|
|
pub(crate) fn to_file_format(self) -> FileFormat {
|
|
|
|
match self {
|
|
|
|
Self::Gif => FileFormat::Video(VideoFormat::Gif),
|
|
|
|
Self::Mp4 => FileFormat::Video(VideoFormat::Mp4),
|
|
|
|
Self::Webm => FileFormat::Video(VideoFormat::Webm),
|
|
|
|
Self::Jpeg => FileFormat::Image(ImageFormat::Jpeg),
|
|
|
|
Self::Png => FileFormat::Image(ImageFormat::Png),
|
|
|
|
Self::Webp => FileFormat::Image(ImageFormat::Webp),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl VideoFormat {
|
2022-10-01 01:00:14 +00:00
|
|
|
const fn to_file_extension(self) -> &'static str {
|
2021-08-31 02:19:47 +00:00
|
|
|
match self {
|
2022-10-01 00:38:11 +00:00
|
|
|
Self::Gif => ".gif",
|
|
|
|
Self::Mp4 => ".mp4",
|
|
|
|
Self::Webm => ".webm",
|
2022-09-26 00:34:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-26 01:39:09 +00:00
|
|
|
fn to_mime(self) -> mime::Mime {
|
2022-09-26 00:34:51 +00:00
|
|
|
match self {
|
2022-09-26 01:39:09 +00:00
|
|
|
Self::Gif => mime::IMAGE_GIF,
|
|
|
|
Self::Mp4 => crate::magick::video_mp4(),
|
|
|
|
Self::Webm => crate::magick::video_webm(),
|
2021-08-31 02:19:47 +00:00
|
|
|
}
|
|
|
|
}
|
2021-08-28 22:15:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ThumbnailFormat {
|
2022-10-01 00:38:11 +00:00
|
|
|
const fn as_ffmpeg_codec(self) -> &'static str {
|
|
|
|
match self {
|
|
|
|
Self::Jpeg => "mjpeg",
|
|
|
|
// Self::Webp => "webp",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-01 01:00:14 +00:00
|
|
|
const fn to_file_extension(self) -> &'static str {
|
2022-10-01 00:38:11 +00:00
|
|
|
match self {
|
|
|
|
Self::Jpeg => ".jpeg",
|
|
|
|
// Self::Webp => ".webp",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const fn as_ffmpeg_format(self) -> &'static str {
|
|
|
|
match self {
|
|
|
|
Self::Jpeg => "image2",
|
|
|
|
// 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 {
|
2021-08-28 22:15:14 +00:00
|
|
|
match self {
|
2022-10-01 00:38:11 +00:00
|
|
|
Self::Mp4 => AudioCodec::Aac,
|
|
|
|
Self::Webm => AudioCodec::Opus,
|
2021-08-28 22:15:14 +00:00
|
|
|
}
|
|
|
|
}
|
2022-10-01 01:00:14 +00:00
|
|
|
|
|
|
|
const fn to_file_extension(self) -> &'static str {
|
|
|
|
match self {
|
|
|
|
Self::Mp4 => ".mp4",
|
|
|
|
Self::Webm => ".webm",
|
|
|
|
}
|
|
|
|
}
|
2022-10-01 00:38:11 +00:00
|
|
|
}
|
2021-08-28 22:15:14 +00:00
|
|
|
|
2022-10-01 00:38:11 +00:00
|
|
|
impl VideoCodec {
|
|
|
|
const fn to_output_format(self) -> OutputFormat {
|
2021-10-23 19:14:12 +00:00
|
|
|
match self {
|
2022-10-01 00:38:11 +00:00
|
|
|
Self::H264 | Self::H265 => OutputFormat::Mp4,
|
|
|
|
Self::Av1 | Self::Vp8 | Self::Vp9 => OutputFormat::Webm,
|
2021-10-23 19:14:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-01 00:38:11 +00:00
|
|
|
const fn to_ffmpeg_codec(self) -> &'static str {
|
2021-08-28 22:15:14 +00:00
|
|
|
match self {
|
2022-10-01 00:38:11 +00:00
|
|
|
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",
|
2021-08-28 22:15:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-02 02:17:18 +00:00
|
|
|
const FORMAT_MAPPINGS: &[(&str, VideoFormat)] = &[
|
|
|
|
("gif", VideoFormat::Gif),
|
|
|
|
("mp4", VideoFormat::Mp4),
|
|
|
|
("webm", VideoFormat::Webm),
|
2022-09-26 00:34:51 +00:00
|
|
|
];
|
|
|
|
|
2022-09-26 01:39:09 +00:00
|
|
|
pub(crate) async fn input_type_bytes(input: Bytes) -> Result<Option<ValidInputType>, Error> {
|
|
|
|
if let Some(details) = details_bytes(input).await? {
|
|
|
|
return Ok(Some(details.validate_input()?));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) async fn details_store<S: Store>(
|
|
|
|
store: &S,
|
|
|
|
identifier: &S::Identifier,
|
|
|
|
) -> Result<Option<Details>, Error> {
|
2022-09-30 22:43:40 +00:00
|
|
|
details_file(move |mut tmp_one| async move {
|
|
|
|
let stream = store.to_stream(identifier, None, None).await?;
|
|
|
|
tmp_one.write_from_stream(stream).await?;
|
|
|
|
Ok(tmp_one)
|
|
|
|
})
|
|
|
|
.await
|
2022-09-26 01:39:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) async fn details_bytes(input: Bytes) -> Result<Option<Details>, Error> {
|
2022-09-30 22:43:40 +00:00
|
|
|
details_file(move |mut tmp_one| async move {
|
|
|
|
tmp_one.write_from_bytes(input).await?;
|
|
|
|
Ok(tmp_one)
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2022-10-02 02:17:18 +00:00
|
|
|
#[tracing::instrument(skip(f))]
|
2022-09-30 22:43:40 +00:00
|
|
|
async fn details_file<F, Fut>(f: F) -> Result<Option<Details>, Error>
|
|
|
|
where
|
|
|
|
F: FnOnce(crate::file::File) -> Fut,
|
|
|
|
Fut: std::future::Future<Output = Result<crate::file::File, Error>>,
|
|
|
|
{
|
2022-09-26 00:34:51 +00:00
|
|
|
let input_file = crate::tmp_file::tmp_file(None);
|
|
|
|
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&input_file).await?;
|
|
|
|
|
2022-09-30 22:43:40 +00:00
|
|
|
let tmp_one = crate::file::File::create(&input_file).await?;
|
|
|
|
let tmp_one = (f)(tmp_one).await?;
|
2022-09-26 00:34:51 +00:00
|
|
|
tmp_one.close().await?;
|
|
|
|
|
2022-09-26 01:39:09 +00:00
|
|
|
let process = Process::run(
|
|
|
|
"ffprobe",
|
|
|
|
&[
|
2022-09-26 00:34:51 +00:00
|
|
|
"-v",
|
|
|
|
"quiet",
|
2022-09-26 01:39:09 +00:00
|
|
|
"-select_streams",
|
|
|
|
"v:0",
|
|
|
|
"-count_frames",
|
2022-09-26 00:34:51 +00:00
|
|
|
"-show_entries",
|
2022-09-26 01:39:09 +00:00
|
|
|
"stream=width,height,nb_read_frames:format=format_name",
|
2022-09-26 00:34:51 +00:00
|
|
|
"-of",
|
|
|
|
"default=noprint_wrappers=1:nokey=1",
|
|
|
|
input_file_str,
|
2022-09-26 01:39:09 +00:00
|
|
|
],
|
|
|
|
)?;
|
2022-09-26 00:34:51 +00:00
|
|
|
|
|
|
|
let mut output = Vec::new();
|
|
|
|
process.read().read_to_end(&mut output).await?;
|
2022-09-26 01:39:09 +00:00
|
|
|
let output = String::from_utf8_lossy(&output);
|
|
|
|
tokio::fs::remove_file(input_file_str).await?;
|
|
|
|
|
|
|
|
parse_details(output)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_details(output: std::borrow::Cow<'_, str>) -> Result<Option<Details>, Error> {
|
2022-10-02 02:17:18 +00:00
|
|
|
tracing::debug!("OUTPUT: {}", output);
|
2022-09-26 01:39:09 +00:00
|
|
|
|
|
|
|
let mut lines = output.lines();
|
|
|
|
|
|
|
|
let width = match lines.next() {
|
|
|
|
Some(line) => line,
|
|
|
|
None => return Ok(None),
|
|
|
|
};
|
2022-09-26 00:34:51 +00:00
|
|
|
|
2022-09-26 01:39:09 +00:00
|
|
|
let height = match lines.next() {
|
|
|
|
Some(line) => line,
|
|
|
|
None => return Ok(None),
|
|
|
|
};
|
|
|
|
|
|
|
|
let frames = match lines.next() {
|
|
|
|
Some(line) => line,
|
|
|
|
None => return Ok(None),
|
|
|
|
};
|
|
|
|
|
|
|
|
let formats = match lines.next() {
|
|
|
|
Some(line) => line,
|
|
|
|
None => return Ok(None),
|
|
|
|
};
|
2022-09-26 00:34:51 +00:00
|
|
|
|
|
|
|
for (k, v) in FORMAT_MAPPINGS {
|
|
|
|
if formats.contains(k) {
|
2022-09-26 01:39:09 +00:00
|
|
|
return Ok(Some(parse_details_inner(width, height, frames, *v)?));
|
2022-09-26 00:34:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
}
|
|
|
|
|
2022-09-26 01:39:09 +00:00
|
|
|
fn parse_details_inner(
|
|
|
|
width: &str,
|
|
|
|
height: &str,
|
|
|
|
frames: &str,
|
2022-10-02 02:17:18 +00:00
|
|
|
format: VideoFormat,
|
2022-09-26 01:39:09 +00:00
|
|
|
) -> Result<Details, Error> {
|
|
|
|
let width = width.parse().map_err(|_| UploadError::UnsupportedFormat)?;
|
|
|
|
let height = height.parse().map_err(|_| UploadError::UnsupportedFormat)?;
|
|
|
|
let frames = frames.parse().map_err(|_| UploadError::UnsupportedFormat)?;
|
|
|
|
|
|
|
|
Ok(Details {
|
|
|
|
mime_type: format.to_mime(),
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
frames: Some(frames),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-10-02 02:17:18 +00:00
|
|
|
#[tracing::instrument(skip(input))]
|
2022-10-01 00:38:11 +00:00
|
|
|
pub(crate) async fn trancsocde_bytes(
|
2021-09-04 00:53:53 +00:00
|
|
|
input: Bytes,
|
2022-10-02 02:17:18 +00:00
|
|
|
input_format: VideoFormat,
|
2022-09-25 23:16:37 +00:00
|
|
|
permit_audio: bool,
|
2022-10-01 00:38:11 +00:00
|
|
|
video_codec: VideoCodec,
|
|
|
|
audio_codec: Option<AudioCodec>,
|
2021-10-23 19:14:12 +00:00
|
|
|
) -> Result<impl AsyncRead + Unpin, Error> {
|
2022-10-01 01:00:14 +00:00
|
|
|
let input_file = crate::tmp_file::tmp_file(Some(input_format.to_file_extension()));
|
2021-10-23 19:14:12 +00:00
|
|
|
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&input_file).await?;
|
|
|
|
|
2022-10-01 01:00:14 +00:00
|
|
|
let output_file =
|
|
|
|
crate::tmp_file::tmp_file(Some(video_codec.to_output_format().to_file_extension()));
|
2021-10-23 19:14:12 +00:00
|
|
|
let output_file_str = output_file.to_str().ok_or(UploadError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&output_file).await?;
|
|
|
|
|
|
|
|
let mut tmp_one = crate::file::File::create(&input_file).await?;
|
|
|
|
tmp_one.write_from_bytes(input).await?;
|
|
|
|
tmp_one.close().await?;
|
|
|
|
|
2022-10-01 00:38:11 +00:00
|
|
|
let output_format = video_codec.to_output_format();
|
2022-10-01 01:02:46 +00:00
|
|
|
let audio_codec = audio_codec.unwrap_or_else(|| output_format.default_audio_codec());
|
2022-10-01 00:38:11 +00:00
|
|
|
|
2022-09-25 23:16:37 +00:00
|
|
|
let process = if permit_audio {
|
2022-09-26 01:39:09 +00:00
|
|
|
Process::run(
|
|
|
|
"ffmpeg",
|
|
|
|
&[
|
|
|
|
"-i",
|
|
|
|
input_file_str,
|
|
|
|
"-pix_fmt",
|
|
|
|
"yuv420p",
|
|
|
|
"-vf",
|
|
|
|
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
|
|
"-c:a",
|
2022-10-01 00:38:11 +00:00
|
|
|
audio_codec.to_ffmpeg_codec(),
|
2022-09-26 01:39:09 +00:00
|
|
|
"-c:v",
|
2022-10-01 00:38:11 +00:00
|
|
|
video_codec.to_ffmpeg_codec(),
|
2022-09-26 01:39:09 +00:00
|
|
|
"-f",
|
2022-10-01 00:38:11 +00:00
|
|
|
output_format.to_ffmpeg_format(),
|
2022-09-26 01:39:09 +00:00
|
|
|
output_file_str,
|
|
|
|
],
|
|
|
|
)?
|
2022-09-25 23:16:37 +00:00
|
|
|
} else {
|
2022-09-26 01:39:09 +00:00
|
|
|
Process::run(
|
|
|
|
"ffmpeg",
|
|
|
|
&[
|
|
|
|
"-i",
|
|
|
|
input_file_str,
|
|
|
|
"-pix_fmt",
|
|
|
|
"yuv420p",
|
|
|
|
"-vf",
|
|
|
|
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
|
|
"-an",
|
|
|
|
"-c:v",
|
2022-10-01 00:38:11 +00:00
|
|
|
video_codec.to_ffmpeg_codec(),
|
2022-09-26 01:39:09 +00:00
|
|
|
"-f",
|
2022-10-01 00:38:11 +00:00
|
|
|
output_format.to_ffmpeg_format(),
|
2022-09-26 01:39:09 +00:00
|
|
|
output_file_str,
|
|
|
|
],
|
|
|
|
)?
|
2022-09-25 23:16:37 +00:00
|
|
|
};
|
2021-09-04 00:53:53 +00:00
|
|
|
|
2021-10-23 19:14:12 +00:00
|
|
|
process.wait().await?;
|
|
|
|
tokio::fs::remove_file(input_file).await?;
|
|
|
|
|
|
|
|
let tmp_two = crate::file::File::open(&output_file).await?;
|
|
|
|
let stream = tmp_two.read_to_stream(None, None).await?;
|
|
|
|
let reader = tokio_util::io::StreamReader::new(stream);
|
|
|
|
let clean_reader = crate::tmp_file::cleanup_tmpfile(reader, output_file);
|
|
|
|
|
|
|
|
Ok(Box::pin(clean_reader))
|
2021-09-04 00:53:53 +00:00
|
|
|
}
|
|
|
|
|
2022-10-02 02:17:18 +00:00
|
|
|
#[tracing::instrument]
|
2021-10-23 04:48:56 +00:00
|
|
|
pub(crate) async fn thumbnail<S: Store>(
|
|
|
|
store: S,
|
|
|
|
from: S::Identifier,
|
2022-10-02 02:17:18 +00:00
|
|
|
input_format: VideoFormat,
|
2021-08-28 22:15:14 +00:00
|
|
|
format: ThumbnailFormat,
|
2022-03-27 01:45:12 +00:00
|
|
|
) -> Result<impl AsyncRead + Unpin, Error> {
|
2022-10-01 01:00:14 +00:00
|
|
|
let input_file = crate::tmp_file::tmp_file(Some(input_format.to_file_extension()));
|
2021-10-23 19:14:12 +00:00
|
|
|
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&input_file).await?;
|
|
|
|
|
2022-10-01 01:00:14 +00:00
|
|
|
let output_file = crate::tmp_file::tmp_file(Some(format.to_file_extension()));
|
2021-10-23 19:14:12 +00:00
|
|
|
let output_file_str = output_file.to_str().ok_or(UploadError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&output_file).await?;
|
|
|
|
|
|
|
|
let mut tmp_one = crate::file::File::create(&input_file).await?;
|
|
|
|
tmp_one
|
|
|
|
.write_from_stream(store.to_stream(&from, None, None).await?)
|
|
|
|
.await?;
|
|
|
|
tmp_one.close().await?;
|
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
let process = Process::run(
|
|
|
|
"ffmpeg",
|
|
|
|
&[
|
|
|
|
"-i",
|
2021-10-28 04:06:03 +00:00
|
|
|
input_file_str,
|
2021-10-23 04:48:56 +00:00
|
|
|
"-vframes",
|
|
|
|
"1",
|
|
|
|
"-codec",
|
2022-10-01 00:38:11 +00:00
|
|
|
format.as_ffmpeg_codec(),
|
2021-10-23 04:48:56 +00:00
|
|
|
"-f",
|
2022-10-01 00:38:11 +00:00
|
|
|
format.as_ffmpeg_format(),
|
2021-10-28 04:06:03 +00:00
|
|
|
output_file_str,
|
2021-10-23 04:48:56 +00:00
|
|
|
],
|
|
|
|
)?;
|
2021-08-26 02:46:11 +00:00
|
|
|
|
2021-10-23 19:14:12 +00:00
|
|
|
process.wait().await?;
|
|
|
|
tokio::fs::remove_file(input_file).await?;
|
|
|
|
|
|
|
|
let tmp_two = crate::file::File::open(&output_file).await?;
|
|
|
|
let stream = tmp_two.read_to_stream(None, None).await?;
|
|
|
|
let reader = tokio_util::io::StreamReader::new(stream);
|
|
|
|
let clean_reader = crate::tmp_file::cleanup_tmpfile(reader, output_file);
|
|
|
|
|
|
|
|
Ok(Box::pin(clean_reader))
|
2021-08-26 02:46:11 +00:00
|
|
|
}
|