mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 11:21:24 +00:00
Remove transcode from animation to video, make video transcoding 'optional'
Video transcoding still happens, but in many cases the video stream is able to be copied verbatim rather than being decoded & encoded
This commit is contained in:
parent
08fd96c2f7
commit
b48a9233b2
26 changed files with 858 additions and 716 deletions
|
@ -1,6 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::primitives::{LogFormat, Targets},
|
config::primitives::{LogFormat, Targets},
|
||||||
formats::VideoCodec,
|
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
};
|
};
|
||||||
use std::{net::SocketAddr, path::PathBuf};
|
use std::{net::SocketAddr, path::PathBuf};
|
||||||
|
@ -120,7 +119,6 @@ struct VideoDefaults {
|
||||||
max_area: u32,
|
max_area: u32,
|
||||||
max_frame_count: u32,
|
max_frame_count: u32,
|
||||||
max_file_size: usize,
|
max_file_size: usize,
|
||||||
video_codec: VideoCodec,
|
|
||||||
quality: VideoQualityDefaults,
|
quality: VideoQualityDefaults,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,7 +281,6 @@ impl Default for VideoDefaults {
|
||||||
max_area: 8_294_400,
|
max_area: 8_294_400,
|
||||||
max_frame_count: 900,
|
max_frame_count: 900,
|
||||||
max_file_size: 40,
|
max_file_size: 40,
|
||||||
video_codec: VideoCodec::Vp9,
|
|
||||||
quality: VideoQualityDefaults::default(),
|
quality: VideoQualityDefaults::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -300,7 +300,8 @@ pub(crate) struct Video {
|
||||||
|
|
||||||
pub(crate) max_frame_count: u32,
|
pub(crate) max_frame_count: u32,
|
||||||
|
|
||||||
pub(crate) video_codec: VideoCodec,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub(crate) video_codec: Option<VideoCodec>,
|
||||||
|
|
||||||
pub(crate) quality: VideoQuality,
|
pub(crate) quality: VideoQuality,
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
discover::DiscoveryLite,
|
bytes_stream::BytesStream,
|
||||||
|
discover::Discovery,
|
||||||
error::Error,
|
error::Error,
|
||||||
formats::{InternalFormat, InternalVideoFormat},
|
formats::{InternalFormat, InternalVideoFormat},
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
store::Store,
|
store::Store,
|
||||||
|
stream::IntoStreamer,
|
||||||
};
|
};
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||||
|
@ -35,14 +37,19 @@ impl Details {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn from_bytes(timeout: u64, input: web::Bytes) -> Result<Self, Error> {
|
pub(crate) async fn from_bytes(timeout: u64, input: web::Bytes) -> Result<Self, Error> {
|
||||||
let DiscoveryLite {
|
let Discovery {
|
||||||
format,
|
input,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frames,
|
frames,
|
||||||
} = crate::discover::discover_bytes_lite(timeout, input).await?;
|
} = crate::discover::discover_bytes(timeout, input).await?;
|
||||||
|
|
||||||
Ok(Details::from_parts(format, width, height, frames))
|
Ok(Details::from_parts(
|
||||||
|
input.internal_format(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn from_store<S: Store>(
|
pub(crate) async fn from_store<S: Store>(
|
||||||
|
@ -50,14 +57,20 @@ impl Details {
|
||||||
identifier: &S::Identifier,
|
identifier: &S::Identifier,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let DiscoveryLite {
|
let mut buf = BytesStream::new();
|
||||||
format,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
} = crate::discover::discover_store_lite(store, identifier, timeout).await?;
|
|
||||||
|
|
||||||
Ok(Details::from_parts(format, width, height, frames))
|
let mut stream = store
|
||||||
|
.to_stream(identifier, None, None)
|
||||||
|
.await?
|
||||||
|
.into_streamer();
|
||||||
|
|
||||||
|
while let Some(res) = stream.next().await {
|
||||||
|
buf.add_bytes(res?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = buf.into_bytes();
|
||||||
|
|
||||||
|
Self::from_bytes(timeout, bytes).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn internal_format(&self) -> InternalFormat {
|
pub(crate) fn internal_format(&self) -> InternalFormat {
|
||||||
|
|
|
@ -4,10 +4,7 @@ mod magick;
|
||||||
|
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
|
|
||||||
use crate::{
|
use crate::formats::InputFile;
|
||||||
formats::{InputFile, InternalFormat},
|
|
||||||
store::Store,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct Discovery {
|
pub(crate) struct Discovery {
|
||||||
|
@ -17,14 +14,6 @@ pub(crate) struct Discovery {
|
||||||
pub(crate) frames: Option<u32>,
|
pub(crate) frames: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub(crate) struct DiscoveryLite {
|
|
||||||
pub(crate) format: InternalFormat,
|
|
||||||
pub(crate) width: u16,
|
|
||||||
pub(crate) height: u16,
|
|
||||||
pub(crate) frames: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub(crate) enum DiscoverError {
|
pub(crate) enum DiscoverError {
|
||||||
#[error("No frames in uploaded media")]
|
#[error("No frames in uploaded media")]
|
||||||
|
@ -37,41 +26,6 @@ pub(crate) enum DiscoverError {
|
||||||
UnsupportedFileType(String),
|
UnsupportedFileType(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn discover_bytes_lite(
|
|
||||||
timeout: u64,
|
|
||||||
bytes: Bytes,
|
|
||||||
) -> Result<DiscoveryLite, crate::error::Error> {
|
|
||||||
if let Some(discovery) = ffmpeg::discover_bytes_lite(timeout, bytes.clone()).await? {
|
|
||||||
return Ok(discovery);
|
|
||||||
}
|
|
||||||
|
|
||||||
let discovery = magick::discover_bytes_lite(timeout, bytes).await?;
|
|
||||||
|
|
||||||
Ok(discovery)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn discover_store_lite<S>(
|
|
||||||
store: &S,
|
|
||||||
identifier: &S::Identifier,
|
|
||||||
timeout: u64,
|
|
||||||
) -> Result<DiscoveryLite, crate::error::Error>
|
|
||||||
where
|
|
||||||
S: Store,
|
|
||||||
{
|
|
||||||
if let Some(discovery) =
|
|
||||||
ffmpeg::discover_stream_lite(timeout, store.to_stream(identifier, None, None).await?)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
return Ok(discovery);
|
|
||||||
}
|
|
||||||
|
|
||||||
let discovery =
|
|
||||||
magick::discover_stream_lite(timeout, store.to_stream(identifier, None, None).await?)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(discovery)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn discover_bytes(
|
pub(crate) async fn discover_bytes(
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
|
|
|
@ -6,40 +6,134 @@ use std::{collections::HashSet, sync::OnceLock};
|
||||||
use crate::{
|
use crate::{
|
||||||
ffmpeg::FfMpegError,
|
ffmpeg::FfMpegError,
|
||||||
formats::{
|
formats::{
|
||||||
AnimationFormat, ImageFormat, ImageInput, InputFile, InternalFormat, InternalVideoFormat,
|
AlphaCodec, AnimationFormat, ImageFormat, ImageInput, InputFile, InputVideoFormat,
|
||||||
VideoFormat,
|
Mp4AudioCodec, Mp4Codec, WebmAlphaCodec, WebmAudioCodec, WebmCodec,
|
||||||
},
|
},
|
||||||
process::Process,
|
process::Process,
|
||||||
};
|
};
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
use futures_core::Stream;
|
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use super::{Discovery, DiscoveryLite};
|
use super::Discovery;
|
||||||
|
|
||||||
const MP4: &str = "mp4";
|
const MP4: &str = "mp4";
|
||||||
const WEBP: &str = "webp_pipe";
|
|
||||||
|
|
||||||
const FFMPEG_FORMAT_MAPPINGS: &[(&str, InternalFormat)] = &[
|
|
||||||
("apng", InternalFormat::Animation(AnimationFormat::Apng)),
|
|
||||||
("gif", InternalFormat::Animation(AnimationFormat::Gif)),
|
|
||||||
(MP4, InternalFormat::Video(InternalVideoFormat::Mp4)),
|
|
||||||
("png_pipe", InternalFormat::Image(ImageFormat::Png)),
|
|
||||||
("webm", InternalFormat::Video(InternalVideoFormat::Webm)),
|
|
||||||
(WEBP, InternalFormat::Image(ImageFormat::Webp)),
|
|
||||||
];
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct FfMpegDiscovery {
|
struct FfMpegDiscovery {
|
||||||
streams: [FfMpegStream; 1],
|
streams: FfMpegStreams,
|
||||||
format: FfMpegFormat,
|
format: FfMpegFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct FfMpegStream {
|
#[serde(transparent)]
|
||||||
|
struct FfMpegStreams {
|
||||||
|
streams: Vec<FfMpegStream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FfMpegStreams {
|
||||||
|
fn into_parts(self) -> Option<(FfMpegVideoStream, Option<FfMpegAudioStream>)> {
|
||||||
|
let mut video = None;
|
||||||
|
let mut audio = None;
|
||||||
|
|
||||||
|
for stream in self.streams {
|
||||||
|
match stream {
|
||||||
|
FfMpegStream::Video(video_stream) if video.is_none() => {
|
||||||
|
video = Some(video_stream);
|
||||||
|
}
|
||||||
|
FfMpegStream::Audio(audio_stream) if audio.is_none() => {
|
||||||
|
audio = Some(audio_stream);
|
||||||
|
}
|
||||||
|
FfMpegStream::Video(FfMpegVideoStream { codec_name, .. }) => {
|
||||||
|
tracing::info!("Encountered duplicate video stream {codec_name:?}");
|
||||||
|
}
|
||||||
|
FfMpegStream::Audio(FfMpegAudioStream { codec_name, .. }) => {
|
||||||
|
tracing::info!("Encountered duplicate audio stream {codec_name:?}");
|
||||||
|
}
|
||||||
|
FfMpegStream::Unknown { codec_name } => {
|
||||||
|
tracing::info!("Encountered unknown stream {codec_name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video.map(|v| (v, audio))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
enum FfMpegVideoCodec {
|
||||||
|
#[serde(rename = "apng")]
|
||||||
|
Apng,
|
||||||
|
#[serde(rename = "av1")]
|
||||||
|
Av1, // still or animated avif, or av1 video
|
||||||
|
#[serde(rename = "gif")]
|
||||||
|
Gif,
|
||||||
|
#[serde(rename = "h264")]
|
||||||
|
H264,
|
||||||
|
#[serde(rename = "hevc")]
|
||||||
|
Hevc, // h265 video
|
||||||
|
#[serde(rename = "mjpeg")]
|
||||||
|
Mjpeg,
|
||||||
|
#[serde(rename = "jpegxl")]
|
||||||
|
Jpegxl,
|
||||||
|
#[serde(rename = "png")]
|
||||||
|
Png,
|
||||||
|
#[serde(rename = "vp8")]
|
||||||
|
Vp8,
|
||||||
|
#[serde(rename = "vp9")]
|
||||||
|
Vp9,
|
||||||
|
#[serde(rename = "webp")]
|
||||||
|
Webp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
enum FfMpegAudioCodec {
|
||||||
|
#[serde(rename = "aac")]
|
||||||
|
Aac,
|
||||||
|
#[serde(rename = "opus")]
|
||||||
|
Opus,
|
||||||
|
#[serde(rename = "vorbis")]
|
||||||
|
Vorbis,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FrameString {
|
||||||
|
frames: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for FrameString {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
let frames = String::deserialize(deserializer)?
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| D::Error::custom("Invalid frames string"))?;
|
||||||
|
|
||||||
|
Ok(FrameString { frames })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct FfMpegAudioStream {
|
||||||
|
codec_name: FfMpegAudioCodec,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct FfMpegVideoStream {
|
||||||
|
codec_name: FfMpegVideoCodec,
|
||||||
width: u16,
|
width: u16,
|
||||||
height: u16,
|
height: u16,
|
||||||
nb_read_frames: Option<String>,
|
pix_fmt: Option<String>,
|
||||||
|
nb_read_frames: Option<FrameString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum FfMpegStream {
|
||||||
|
Audio(FfMpegAudioStream),
|
||||||
|
Video(FfMpegVideoStream),
|
||||||
|
Unknown { codec_name: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
@ -67,7 +161,7 @@ pub(super) async fn discover_bytes(
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
) -> Result<Option<Discovery>, FfMpegError> {
|
) -> Result<Option<Discovery>, FfMpegError> {
|
||||||
discover_file_full(
|
discover_file(
|
||||||
move |mut file| {
|
move |mut file| {
|
||||||
let bytes = bytes.clone();
|
let bytes = bytes.clone();
|
||||||
|
|
||||||
|
@ -83,130 +177,22 @@ pub(super) async fn discover_bytes(
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn discover_bytes_lite(
|
async fn allows_alpha(pixel_format: &str, timeout: u64) -> Result<bool, FfMpegError> {
|
||||||
timeout: u64,
|
static ALPHA_PIXEL_FORMATS: OnceLock<HashSet<String>> = OnceLock::new();
|
||||||
bytes: Bytes,
|
|
||||||
) -> Result<Option<DiscoveryLite>, FfMpegError> {
|
|
||||||
discover_file_lite(
|
|
||||||
move |mut file| async move {
|
|
||||||
file.write_from_bytes(bytes)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Write)?;
|
|
||||||
Ok(file)
|
|
||||||
},
|
|
||||||
timeout,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn discover_stream_lite<S>(
|
match ALPHA_PIXEL_FORMATS.get() {
|
||||||
timeout: u64,
|
Some(alpha_pixel_formats) => Ok(alpha_pixel_formats.contains(pixel_format)),
|
||||||
stream: S,
|
None => {
|
||||||
) -> Result<Option<DiscoveryLite>, FfMpegError>
|
let pixel_formats = alpha_pixel_formats(timeout).await?;
|
||||||
where
|
let alpha = pixel_formats.contains(pixel_format);
|
||||||
S: Stream<Item = std::io::Result<Bytes>> + Unpin,
|
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
|
||||||
{
|
Ok(alpha)
|
||||||
discover_file_lite(
|
|
||||||
move |mut file| async move {
|
|
||||||
file.write_from_stream(stream)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Write)?;
|
|
||||||
Ok(file)
|
|
||||||
},
|
|
||||||
timeout,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn discover_file_lite<F, Fut>(
|
|
||||||
f: F,
|
|
||||||
timeout: u64,
|
|
||||||
) -> Result<Option<DiscoveryLite>, FfMpegError>
|
|
||||||
where
|
|
||||||
F: FnOnce(crate::file::File) -> Fut,
|
|
||||||
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
|
||||||
{
|
|
||||||
let Some(DiscoveryLite {
|
|
||||||
format,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
}) = discover_file(f, timeout)
|
|
||||||
.await? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
// If we're not confident in our discovery don't return it
|
|
||||||
if width == 0 || height == 0 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(DiscoveryLite {
|
|
||||||
format,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn discover_file_full<F, Fut>(f: F, timeout: u64) -> Result<Option<Discovery>, FfMpegError>
|
|
||||||
where
|
|
||||||
F: Fn(crate::file::File) -> Fut + Clone,
|
|
||||||
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
|
||||||
{
|
|
||||||
let Some(DiscoveryLite { format, width, height, frames }) = discover_file(f.clone(), timeout).await? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
match format {
|
|
||||||
InternalFormat::Video(InternalVideoFormat::Webm) => {
|
|
||||||
static ALPHA_PIXEL_FORMATS: OnceLock<HashSet<String>> = OnceLock::new();
|
|
||||||
|
|
||||||
let format = pixel_format(f, timeout).await?;
|
|
||||||
|
|
||||||
let alpha = match ALPHA_PIXEL_FORMATS.get() {
|
|
||||||
Some(alpha_pixel_formats) => alpha_pixel_formats.contains(&format),
|
|
||||||
None => {
|
|
||||||
let pixel_formats = alpha_pixel_formats(timeout).await?;
|
|
||||||
let alpha = pixel_formats.contains(&format);
|
|
||||||
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
|
|
||||||
alpha
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(Discovery {
|
|
||||||
input: InputFile::Video(VideoFormat::Webm { alpha }),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
InternalFormat::Video(InternalVideoFormat::Mp4) => Ok(Some(Discovery {
|
|
||||||
input: InputFile::Video(VideoFormat::Mp4),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
})),
|
|
||||||
InternalFormat::Animation(format) => Ok(Some(Discovery {
|
|
||||||
input: InputFile::Animation(format),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
})),
|
|
||||||
InternalFormat::Image(format) => Ok(Some(Discovery {
|
|
||||||
input: InputFile::Image(ImageInput {
|
|
||||||
format,
|
|
||||||
needs_reorient: false,
|
|
||||||
}),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(f))]
|
#[tracing::instrument(skip(f))]
|
||||||
async fn discover_file<F, Fut>(f: F, timeout: u64) -> Result<Option<DiscoveryLite>, FfMpegError>
|
async fn discover_file<F, Fut>(f: F, timeout: u64) -> Result<Option<Discovery>, FfMpegError>
|
||||||
where
|
where
|
||||||
F: FnOnce(crate::file::File) -> Fut,
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
||||||
|
@ -228,11 +214,9 @@ where
|
||||||
&[
|
&[
|
||||||
"-v",
|
"-v",
|
||||||
"quiet",
|
"quiet",
|
||||||
"-select_streams",
|
|
||||||
"v:0",
|
|
||||||
"-count_frames",
|
"-count_frames",
|
||||||
"-show_entries",
|
"-show_entries",
|
||||||
"stream=width,height,nb_read_frames:format=format_name",
|
"stream=width,height,nb_read_frames,codec_name,pix_fmt:format=format_name",
|
||||||
"-of",
|
"-of",
|
||||||
"default=noprint_wrappers=1:nokey=1",
|
"default=noprint_wrappers=1:nokey=1",
|
||||||
"-print_format",
|
"-print_format",
|
||||||
|
@ -254,54 +238,23 @@ where
|
||||||
|
|
||||||
let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
||||||
|
|
||||||
parse_discovery(output)
|
let (discovery, pix_fmt) = parse_discovery(output)?;
|
||||||
}
|
|
||||||
|
|
||||||
async fn pixel_format<F, Fut>(f: F, timeout: u64) -> Result<String, FfMpegError>
|
let Some(mut discovery) = discovery else {
|
||||||
where
|
return Ok(None);
|
||||||
F: FnOnce(crate::file::File) -> Fut,
|
};
|
||||||
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
|
||||||
{
|
|
||||||
let input_file = crate::tmp_file::tmp_file(None);
|
|
||||||
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
|
||||||
crate::store::file_store::safe_create_parent(&input_file)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::CreateDir)?;
|
|
||||||
|
|
||||||
let tmp_one = crate::file::File::create(&input_file)
|
if let Some(pixel_format) = pix_fmt {
|
||||||
.await
|
if let InputFile::Video(InputVideoFormat::Webm {
|
||||||
.map_err(FfMpegError::CreateFile)?;
|
video_codec: WebmCodec::Alpha(AlphaCodec { alpha, .. }),
|
||||||
let tmp_one = (f)(tmp_one).await?;
|
..
|
||||||
tmp_one.close().await.map_err(FfMpegError::CloseFile)?;
|
}) = &mut discovery.input
|
||||||
|
{
|
||||||
|
*alpha = allows_alpha(&pixel_format, timeout).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let process = Process::run(
|
Ok(Some(discovery))
|
||||||
"ffprobe",
|
|
||||||
&[
|
|
||||||
"-v",
|
|
||||||
"0",
|
|
||||||
"-select_streams",
|
|
||||||
"v:0",
|
|
||||||
"-show_entries",
|
|
||||||
"stream=pix_fmt",
|
|
||||||
"-of",
|
|
||||||
"compact=p=0:nk=1",
|
|
||||||
input_file_str,
|
|
||||||
],
|
|
||||||
timeout,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut output = Vec::new();
|
|
||||||
process
|
|
||||||
.read()
|
|
||||||
.read_to_end(&mut output)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::Read)?;
|
|
||||||
|
|
||||||
tokio::fs::remove_file(input_file_str)
|
|
||||||
.await
|
|
||||||
.map_err(FfMpegError::RemoveFile)?;
|
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn alpha_pixel_formats(timeout: u64) -> Result<HashSet<String>, FfMpegError> {
|
async fn alpha_pixel_formats(timeout: u64) -> Result<HashSet<String>, FfMpegError> {
|
||||||
|
@ -346,56 +299,145 @@ fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet<String> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_discovery(discovery: FfMpegDiscovery) -> Result<Option<DiscoveryLite>, FfMpegError> {
|
fn is_mp4(format_name: &str) -> bool {
|
||||||
|
format_name.contains(MP4)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mp4_audio_codec(stream: Option<FfMpegAudioStream>) -> Option<Mp4AudioCodec> {
|
||||||
|
match stream {
|
||||||
|
Some(FfMpegAudioStream {
|
||||||
|
codec_name: FfMpegAudioCodec::Aac,
|
||||||
|
}) => Some(Mp4AudioCodec::Aac),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn webm_audio_codec(stream: Option<FfMpegAudioStream>) -> Option<WebmAudioCodec> {
|
||||||
|
match stream {
|
||||||
|
Some(FfMpegAudioStream {
|
||||||
|
codec_name: FfMpegAudioCodec::Opus,
|
||||||
|
}) => Some(WebmAudioCodec::Opus),
|
||||||
|
Some(FfMpegAudioStream {
|
||||||
|
codec_name: FfMpegAudioCodec::Vorbis,
|
||||||
|
}) => Some(WebmAudioCodec::Vorbis),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_discovery(
|
||||||
|
discovery: FfMpegDiscovery,
|
||||||
|
) -> Result<(Option<Discovery>, Option<String>), FfMpegError> {
|
||||||
let FfMpegDiscovery {
|
let FfMpegDiscovery {
|
||||||
streams:
|
streams,
|
||||||
[FfMpegStream {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
nb_read_frames,
|
|
||||||
}],
|
|
||||||
format: FfMpegFormat { format_name },
|
format: FfMpegFormat { format_name },
|
||||||
} = discovery;
|
} = discovery;
|
||||||
|
|
||||||
if let Some((name, value)) = FFMPEG_FORMAT_MAPPINGS
|
let Some((video_stream, audio_stream)) = streams.into_parts() else {
|
||||||
.iter()
|
tracing::info!("No matching format mapping for {format_name}");
|
||||||
.find(|(name, _)| format_name.contains(name))
|
return Ok((None, None));
|
||||||
{
|
};
|
||||||
let frames = nb_read_frames.and_then(|frames| frames.parse().ok());
|
|
||||||
|
|
||||||
if *name == MP4 && frames.map(|nb| nb == 1).unwrap_or(false) {
|
let input = match video_stream.codec_name {
|
||||||
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed mp4 even when
|
FfMpegVideoCodec::Av1
|
||||||
|
if video_stream
|
||||||
|
.nb_read_frames
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|count| count.frames == 1) =>
|
||||||
|
{
|
||||||
|
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed av1 even when
|
||||||
// animated
|
// animated
|
||||||
|
|
||||||
return Ok(Some(DiscoveryLite {
|
return Ok((
|
||||||
format: InternalFormat::Animation(AnimationFormat::Avif),
|
Some(Discovery {
|
||||||
width,
|
input: InputFile::Animation(AnimationFormat::Avif),
|
||||||
height,
|
width: video_stream.width,
|
||||||
frames: None,
|
height: video_stream.height,
|
||||||
}));
|
frames: None,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
FfMpegVideoCodec::Webp
|
||||||
if *name == WEBP && (frames.is_none() || width == 0 || height == 0) {
|
if video_stream.height == 0
|
||||||
|
|| video_stream.width == 0
|
||||||
|
|| video_stream.nb_read_frames.is_none() =>
|
||||||
|
{
|
||||||
// Might be Animated Webp, ffmpeg incorrectly detects animated webp as having no frames
|
// Might be Animated Webp, ffmpeg incorrectly detects animated webp as having no frames
|
||||||
// and 0 dimensions
|
// and 0 dimensions
|
||||||
|
|
||||||
return Ok(Some(DiscoveryLite {
|
return Ok((
|
||||||
format: InternalFormat::Animation(AnimationFormat::Webp),
|
Some(Discovery {
|
||||||
width,
|
input: InputFile::Animation(AnimationFormat::Webp),
|
||||||
height,
|
width: video_stream.width,
|
||||||
frames,
|
height: video_stream.height,
|
||||||
}));
|
frames: None,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
FfMpegVideoCodec::Av1 if is_mp4(&format_name) => InputFile::Video(InputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::Av1,
|
||||||
|
audio_codec: mp4_audio_codec(audio_stream),
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Av1 => InputFile::Video(InputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Av1,
|
||||||
|
audio_codec: webm_audio_codec(audio_stream),
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Apng => InputFile::Animation(AnimationFormat::Apng),
|
||||||
|
FfMpegVideoCodec::Gif => InputFile::Animation(AnimationFormat::Gif),
|
||||||
|
FfMpegVideoCodec::H264 => InputFile::Video(InputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::H264,
|
||||||
|
audio_codec: mp4_audio_codec(audio_stream),
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Hevc => InputFile::Video(InputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::H265,
|
||||||
|
audio_codec: mp4_audio_codec(audio_stream),
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Png => InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Png,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Mjpeg => InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Jpeg,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Jpegxl => InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Jxl,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Vp8 => InputFile::Video(InputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: WebmAlphaCodec::Vp8,
|
||||||
|
}),
|
||||||
|
audio_codec: webm_audio_codec(audio_stream),
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Vp9 => InputFile::Video(InputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: WebmAlphaCodec::Vp9,
|
||||||
|
}),
|
||||||
|
audio_codec: webm_audio_codec(audio_stream),
|
||||||
|
}),
|
||||||
|
FfMpegVideoCodec::Webp => InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Webp,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
return Ok(Some(DiscoveryLite {
|
Ok((
|
||||||
format: *value,
|
Some(Discovery {
|
||||||
width,
|
input,
|
||||||
height,
|
width: video_stream.width,
|
||||||
frames: frames.and_then(|frames| if frames > 1 { Some(frames) } else { None }),
|
height: video_stream.height,
|
||||||
}));
|
frames: video_stream.nb_read_frames.and_then(|f| {
|
||||||
}
|
if f.frames <= 1 {
|
||||||
|
None
|
||||||
tracing::info!("No matching format mapping for {format_name}");
|
} else {
|
||||||
|
Some(f.frames)
|
||||||
Ok(None)
|
}
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
video_stream.pix_fmt,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
17
src/discover/ffmpeg/ffprobe_6_0_animated_avif_details.json
Normal file
17
src/discover/ffmpeg/ffprobe_6_0_animated_avif_details.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"programs": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"codec_name": "av1",
|
||||||
|
"width": 112,
|
||||||
|
"height": 112,
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"nb_read_frames": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": {
|
||||||
|
"format_name": "mov,mp4,m4a,3gp,3g2,mj2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
|
"codec_name": "webp",
|
||||||
"width": 0,
|
"width": 0,
|
||||||
"height": 0
|
"height": 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
|
"codec_name": "apng",
|
||||||
"width": 112,
|
"width": 112,
|
||||||
"height": 112,
|
"height": 112,
|
||||||
|
"pix_fmt": "rgba",
|
||||||
"nb_read_frames": "27"
|
"nb_read_frames": "27"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"width": 1920,
|
"codec_name": "av1",
|
||||||
"height": 1080,
|
"width": 1200,
|
||||||
|
"height": 1387,
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
"nb_read_frames": "1"
|
"nb_read_frames": "1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,9 +4,11 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"width": 160,
|
"codec_name": "gif",
|
||||||
"height": 227,
|
"width": 112,
|
||||||
"nb_read_frames": "28"
|
"height": 112,
|
||||||
|
"pix_fmt": "bgra",
|
||||||
|
"nb_read_frames": "27"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": {
|
"format": {
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"width": 1920,
|
"codec_name": "mjpeg",
|
||||||
"height": 1080,
|
"width": 1663,
|
||||||
|
"height": 1247,
|
||||||
|
"pix_fmt": "yuvj420p",
|
||||||
"nb_read_frames": "1"
|
"nb_read_frames": "1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
|
"codec_name": "jpegxl",
|
||||||
"width": 0,
|
"width": 0,
|
||||||
"height": 0
|
"height": 0
|
||||||
}
|
}
|
||||||
|
|
17
src/discover/ffmpeg/ffprobe_6_0_mp4_av1_details.json
Normal file
17
src/discover/ffmpeg/ffprobe_6_0_mp4_av1_details.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"programs": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"codec_name": "av1",
|
||||||
|
"width": 112,
|
||||||
|
"height": 112,
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"nb_read_frames": "27"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": {
|
||||||
|
"format_name": "mov,mp4,m4a,3gp,3g2,mj2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,11 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"width": 852,
|
"codec_name": "h264",
|
||||||
"height": 480,
|
"width": 1426,
|
||||||
"nb_read_frames": "35364"
|
"height": 834,
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"nb_read_frames": "105"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": {
|
"format": {
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
|
"codec_name": "png",
|
||||||
"width": 450,
|
"width": 450,
|
||||||
"height": 401,
|
"height": 401,
|
||||||
|
"pix_fmt": "rgb24",
|
||||||
"nb_read_frames": "1"
|
"nb_read_frames": "1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
|
"codec_name": "av1",
|
||||||
"width": 112,
|
"width": 112,
|
||||||
"height": 112,
|
"height": 112,
|
||||||
|
"pix_fmt": "gbrp",
|
||||||
"nb_read_frames": "27"
|
"nb_read_frames": "27"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,9 +4,11 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"width": 640,
|
"codec_name": "vp9",
|
||||||
"height": 480,
|
"width": 112,
|
||||||
"nb_read_frames": "34650"
|
"height": 112,
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"nb_read_frames": "27"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": {
|
"format": {
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
],
|
],
|
||||||
"streams": [
|
"streams": [
|
||||||
{
|
{
|
||||||
"width": 1920,
|
"codec_name": "webp",
|
||||||
"height": 1080,
|
"width": 1200,
|
||||||
|
"height": 1387,
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
"nb_read_frames": "1"
|
"nb_read_frames": "1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,22 +1,34 @@
|
||||||
use crate::formats::{AnimationFormat, ImageFormat, InternalFormat, InternalVideoFormat};
|
use crate::formats::{
|
||||||
|
AlphaCodec, AnimationFormat, ImageFormat, ImageInput, InputFile, InputVideoFormat, Mp4Codec,
|
||||||
|
WebmAlphaCodec, WebmCodec,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{DiscoveryLite, FfMpegDiscovery, PixelFormatOutput};
|
use super::{Discovery, FfMpegDiscovery, PixelFormatOutput};
|
||||||
|
|
||||||
fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
|
fn details_tests() -> [(&'static str, Option<Discovery>); 13] {
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"animated_webp",
|
"animated_webp",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Animation(AnimationFormat::Webp),
|
input: InputFile::Animation(AnimationFormat::Webp),
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
frames: None,
|
frames: None,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"animated_avif",
|
||||||
|
Some(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationFormat::Avif),
|
||||||
|
width: 112,
|
||||||
|
height: 112,
|
||||||
|
frames: None,
|
||||||
|
}),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"apng",
|
"apng",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Animation(AnimationFormat::Apng),
|
input: InputFile::Animation(AnimationFormat::Apng),
|
||||||
width: 112,
|
width: 112,
|
||||||
height: 112,
|
height: 112,
|
||||||
frames: Some(27),
|
frames: Some(27),
|
||||||
|
@ -24,37 +36,77 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"avif",
|
"avif",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Animation(AnimationFormat::Avif),
|
input: InputFile::Animation(AnimationFormat::Avif),
|
||||||
width: 1920,
|
width: 1200,
|
||||||
height: 1080,
|
height: 1387,
|
||||||
frames: None,
|
frames: None,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"gif",
|
"gif",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Animation(AnimationFormat::Gif),
|
input: InputFile::Animation(AnimationFormat::Gif),
|
||||||
width: 160,
|
width: 112,
|
||||||
height: 227,
|
height: 112,
|
||||||
frames: Some(28),
|
frames: Some(27),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"jpeg",
|
||||||
|
Some(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Jpeg,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width: 1663,
|
||||||
|
height: 1247,
|
||||||
|
frames: None,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"jxl",
|
||||||
|
Some(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Jxl,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
frames: None,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
("jpeg", None),
|
|
||||||
("jxl", None),
|
|
||||||
(
|
(
|
||||||
"mp4",
|
"mp4",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Video(InternalVideoFormat::Mp4),
|
input: InputFile::Video(InputVideoFormat::Mp4 {
|
||||||
width: 852,
|
video_codec: Mp4Codec::H264,
|
||||||
height: 480,
|
audio_codec: None,
|
||||||
frames: Some(35364),
|
}),
|
||||||
|
width: 1426,
|
||||||
|
height: 834,
|
||||||
|
frames: Some(105),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mp4_av1",
|
||||||
|
Some(Discovery {
|
||||||
|
input: InputFile::Video(InputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::Av1,
|
||||||
|
audio_codec: None,
|
||||||
|
}),
|
||||||
|
width: 112,
|
||||||
|
height: 112,
|
||||||
|
frames: Some(27),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"png",
|
"png",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Image(ImageFormat::Png),
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Png,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
width: 450,
|
width: 450,
|
||||||
height: 401,
|
height: 401,
|
||||||
frames: None,
|
frames: None,
|
||||||
|
@ -62,17 +114,26 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"webm",
|
"webm",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Video(InternalVideoFormat::Webm),
|
input: InputFile::Video(InputVideoFormat::Webm {
|
||||||
width: 640,
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
height: 480,
|
alpha: false,
|
||||||
frames: Some(34650),
|
codec: WebmAlphaCodec::Vp9,
|
||||||
|
}),
|
||||||
|
audio_codec: None,
|
||||||
|
}),
|
||||||
|
width: 112,
|
||||||
|
height: 112,
|
||||||
|
frames: Some(27),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"webm_av1",
|
"webm_av1",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Video(InternalVideoFormat::Webm),
|
input: InputFile::Video(InputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Av1,
|
||||||
|
audio_codec: None,
|
||||||
|
}),
|
||||||
width: 112,
|
width: 112,
|
||||||
height: 112,
|
height: 112,
|
||||||
frames: Some(27),
|
frames: Some(27),
|
||||||
|
@ -80,10 +141,13 @@ fn details_tests() -> [(&'static str, Option<DiscoveryLite>); 11] {
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"webp",
|
"webp",
|
||||||
Some(DiscoveryLite {
|
Some(Discovery {
|
||||||
format: InternalFormat::Image(ImageFormat::Webp),
|
input: InputFile::Image(ImageInput {
|
||||||
width: 1920,
|
format: ImageFormat::Webp,
|
||||||
height: 1080,
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width: 1200,
|
||||||
|
height: 1387,
|
||||||
frames: None,
|
frames: None,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -100,7 +164,7 @@ fn parse_discovery() {
|
||||||
|
|
||||||
let json: FfMpegDiscovery = serde_json::from_str(&string).expect("Valid json");
|
let json: FfMpegDiscovery = serde_json::from_str(&string).expect("Valid json");
|
||||||
|
|
||||||
let output = super::parse_discovery(json).expect("Parsed details");
|
let (output, _) = super::parse_discovery(json).expect("Parsed details");
|
||||||
|
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,16 @@
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
use futures_core::Stream;
|
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
discover::DiscoverError,
|
discover::DiscoverError,
|
||||||
formats::{AnimationFormat, ImageFormat, ImageInput, InputFile, VideoFormat},
|
formats::{AnimationFormat, ImageFormat, ImageInput, InputFile},
|
||||||
magick::MagickError,
|
magick::MagickError,
|
||||||
process::Process,
|
process::Process,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Discovery, DiscoveryLite};
|
use super::Discovery;
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct MagickDiscovery {
|
struct MagickDiscovery {
|
||||||
|
@ -31,59 +30,6 @@ struct Geometry {
|
||||||
height: u16,
|
height: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Discovery {
|
|
||||||
fn lite(self) -> DiscoveryLite {
|
|
||||||
let Discovery {
|
|
||||||
input,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
DiscoveryLite {
|
|
||||||
format: input.internal_format(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn discover_bytes_lite(
|
|
||||||
timeout: u64,
|
|
||||||
bytes: Bytes,
|
|
||||||
) -> Result<DiscoveryLite, MagickError> {
|
|
||||||
discover_file_lite(
|
|
||||||
move |mut file| async move {
|
|
||||||
file.write_from_bytes(bytes)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::Write)?;
|
|
||||||
Ok(file)
|
|
||||||
},
|
|
||||||
timeout,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn discover_stream_lite<S>(
|
|
||||||
timeout: u64,
|
|
||||||
stream: S,
|
|
||||||
) -> Result<DiscoveryLite, MagickError>
|
|
||||||
where
|
|
||||||
S: Stream<Item = std::io::Result<Bytes>> + Unpin + 'static,
|
|
||||||
{
|
|
||||||
discover_file_lite(
|
|
||||||
move |mut file| async move {
|
|
||||||
file.write_from_stream(stream)
|
|
||||||
.await
|
|
||||||
.map_err(MagickError::Write)?;
|
|
||||||
Ok(file)
|
|
||||||
},
|
|
||||||
timeout,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn confirm_bytes(
|
pub(super) async fn confirm_bytes(
|
||||||
discovery: Option<Discovery>,
|
discovery: Option<Discovery>,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
|
@ -107,6 +53,18 @@ pub(super) async fn confirm_bytes(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if frames == 1 {
|
||||||
|
return Ok(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Avif,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(Discovery {
|
return Ok(Discovery {
|
||||||
input: InputFile::Animation(AnimationFormat::Avif),
|
input: InputFile::Animation(AnimationFormat::Avif),
|
||||||
width,
|
width,
|
||||||
|
@ -189,14 +147,6 @@ where
|
||||||
Ok(lines)
|
Ok(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn discover_file_lite<F, Fut>(f: F, timeout: u64) -> Result<DiscoveryLite, MagickError>
|
|
||||||
where
|
|
||||||
F: FnOnce(crate::file::File) -> Fut,
|
|
||||||
Fut: std::future::Future<Output = Result<crate::file::File, MagickError>>,
|
|
||||||
{
|
|
||||||
discover_file(f, timeout).await.map(Discovery::lite)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn discover_file<F, Fut>(f: F, timeout: u64) -> Result<Discovery, MagickError>
|
async fn discover_file<F, Fut>(f: F, timeout: u64) -> Result<Discovery, MagickError>
|
||||||
where
|
where
|
||||||
F: FnOnce(crate::file::File) -> Fut,
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
|
@ -338,12 +288,6 @@ fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, DiscoverEr
|
||||||
height,
|
height,
|
||||||
frames: None,
|
frames: None,
|
||||||
}),
|
}),
|
||||||
"MP4" => Ok(Discovery {
|
|
||||||
input: InputFile::Video(VideoFormat::Mp4),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames: Some(frames),
|
|
||||||
}),
|
|
||||||
"PNG" => Ok(Discovery {
|
"PNG" => Ok(Discovery {
|
||||||
input: InputFile::Image(ImageInput {
|
input: InputFile::Image(ImageInput {
|
||||||
format: ImageFormat::Png,
|
format: ImageFormat::Png,
|
||||||
|
@ -373,12 +317,6 @@ fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, DiscoverEr
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"WEBM" => Ok(Discovery {
|
|
||||||
input: InputFile::Video(VideoFormat::Webm { alpha: true }),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
frames: Some(frames),
|
|
||||||
}),
|
|
||||||
otherwise => Err(DiscoverError::UnsupportedFileType(String::from(otherwise))),
|
otherwise => Err(DiscoverError::UnsupportedFileType(String::from(otherwise))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use crate::formats::{AnimationFormat, ImageFormat, InternalFormat, InternalVideoFormat};
|
use crate::formats::{AnimationFormat, ImageFormat, ImageInput, InputFile};
|
||||||
|
|
||||||
use super::{DiscoveryLite, MagickDiscovery};
|
use super::{Discovery, MagickDiscovery};
|
||||||
|
|
||||||
fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
fn details_tests() -> [(&'static str, Discovery); 7] {
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"animated_webp",
|
"animated_webp",
|
||||||
DiscoveryLite {
|
Discovery {
|
||||||
format: InternalFormat::Animation(AnimationFormat::Webp),
|
input: InputFile::Animation(AnimationFormat::Webp),
|
||||||
width: 112,
|
width: 112,
|
||||||
height: 112,
|
height: 112,
|
||||||
frames: Some(27),
|
frames: Some(27),
|
||||||
|
@ -15,8 +15,11 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"avif",
|
"avif",
|
||||||
DiscoveryLite {
|
Discovery {
|
||||||
format: InternalFormat::Image(ImageFormat::Avif),
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Avif,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
frames: None,
|
frames: None,
|
||||||
|
@ -24,8 +27,8 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"gif",
|
"gif",
|
||||||
DiscoveryLite {
|
Discovery {
|
||||||
format: InternalFormat::Animation(AnimationFormat::Gif),
|
input: InputFile::Animation(AnimationFormat::Gif),
|
||||||
width: 414,
|
width: 414,
|
||||||
height: 261,
|
height: 261,
|
||||||
frames: Some(17),
|
frames: Some(17),
|
||||||
|
@ -33,8 +36,11 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"jpeg",
|
"jpeg",
|
||||||
DiscoveryLite {
|
Discovery {
|
||||||
format: InternalFormat::Image(ImageFormat::Jpeg),
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Jpeg,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
frames: None,
|
frames: None,
|
||||||
|
@ -42,44 +48,35 @@ fn details_tests() -> [(&'static str, DiscoveryLite); 9] {
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"jxl",
|
"jxl",
|
||||||
DiscoveryLite {
|
Discovery {
|
||||||
format: InternalFormat::Image(ImageFormat::Jxl),
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Jxl,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
frames: None,
|
frames: None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"mp4",
|
|
||||||
DiscoveryLite {
|
|
||||||
format: InternalFormat::Video(InternalVideoFormat::Mp4),
|
|
||||||
width: 414,
|
|
||||||
height: 261,
|
|
||||||
frames: Some(17),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"png",
|
"png",
|
||||||
DiscoveryLite {
|
Discovery {
|
||||||
format: InternalFormat::Image(ImageFormat::Png),
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Png,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
width: 497,
|
width: 497,
|
||||||
height: 694,
|
height: 694,
|
||||||
frames: None,
|
frames: None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"webm",
|
|
||||||
DiscoveryLite {
|
|
||||||
format: InternalFormat::Video(InternalVideoFormat::Webm),
|
|
||||||
width: 112,
|
|
||||||
height: 112,
|
|
||||||
frames: Some(27),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"webp",
|
"webp",
|
||||||
DiscoveryLite {
|
Discovery {
|
||||||
format: InternalFormat::Image(ImageFormat::Webp),
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Webp,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
frames: None,
|
frames: None,
|
||||||
|
@ -98,7 +95,7 @@ fn parse_discovery() {
|
||||||
|
|
||||||
let json: Vec<MagickDiscovery> = serde_json::from_str(&string).expect("Valid json");
|
let json: Vec<MagickDiscovery> = serde_json::from_str(&string).expect("Valid json");
|
||||||
|
|
||||||
let output = super::parse_discovery(json).expect("Parsed details").lite();
|
let output = super::parse_discovery(json).expect("Parsed details");
|
||||||
|
|
||||||
assert_eq!(output, expected);
|
assert_eq!(output, expected);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@ use std::str::FromStr;
|
||||||
pub(crate) use animation::{AnimationFormat, AnimationOutput};
|
pub(crate) use animation::{AnimationFormat, AnimationOutput};
|
||||||
pub(crate) use image::{ImageFormat, ImageInput, ImageOutput};
|
pub(crate) use image::{ImageFormat, ImageInput, ImageOutput};
|
||||||
pub(crate) use video::{
|
pub(crate) use video::{
|
||||||
AudioCodec, InternalVideoFormat, OutputVideoFormat, VideoCodec, VideoFormat,
|
AlphaCodec, AudioCodec, InputVideoFormat, InternalVideoFormat, Mp4AudioCodec, Mp4Codec,
|
||||||
|
OutputVideo, VideoCodec, WebmAlphaCodec, WebmAudioCodec, WebmCodec,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -22,7 +23,7 @@ pub(crate) struct Validations<'a> {
|
||||||
pub(crate) enum InputFile {
|
pub(crate) enum InputFile {
|
||||||
Image(ImageInput),
|
Image(ImageInput),
|
||||||
Animation(AnimationFormat),
|
Animation(AnimationFormat),
|
||||||
Video(VideoFormat),
|
Video(InputVideoFormat),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum InputVideoFormat {
|
||||||
|
Mp4 {
|
||||||
|
video_codec: Mp4Codec,
|
||||||
|
audio_codec: Option<Mp4AudioCodec>,
|
||||||
|
},
|
||||||
|
Webm {
|
||||||
|
video_codec: WebmCodec,
|
||||||
|
audio_codec: Option<WebmAudioCodec>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub(crate) enum VideoFormat {
|
pub(crate) struct OutputVideo {
|
||||||
Mp4,
|
pub(crate) transcode_video: bool,
|
||||||
Webm { alpha: bool },
|
pub(crate) transcode_audio: bool,
|
||||||
|
pub(crate) format: OutputVideoFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
|
@ -70,6 +83,8 @@ pub(crate) enum AudioCodec {
|
||||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
)]
|
)]
|
||||||
pub(crate) enum Mp4Codec {
|
pub(crate) enum Mp4Codec {
|
||||||
|
#[serde(rename = "av1")]
|
||||||
|
Av1,
|
||||||
#[serde(rename = "h264")]
|
#[serde(rename = "h264")]
|
||||||
H264,
|
H264,
|
||||||
#[serde(rename = "h265")]
|
#[serde(rename = "h265")]
|
||||||
|
@ -125,113 +140,262 @@ pub(crate) enum InternalVideoFormat {
|
||||||
Webm,
|
Webm,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoFormat {
|
const fn webm_audio(
|
||||||
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
allow_audio: bool,
|
||||||
|
has_audio: bool,
|
||||||
|
prescribed: Option<AudioCodec>,
|
||||||
|
provided: Option<WebmAudioCodec>,
|
||||||
|
) -> (Option<WebmAudioCodec>, bool) {
|
||||||
|
if allow_audio && has_audio {
|
||||||
|
match prescribed {
|
||||||
|
Some(AudioCodec::Opus) => (
|
||||||
|
Some(WebmAudioCodec::Opus),
|
||||||
|
!matches!(provided, Some(WebmAudioCodec::Opus)),
|
||||||
|
),
|
||||||
|
Some(AudioCodec::Vorbis) => (
|
||||||
|
Some(WebmAudioCodec::Vorbis),
|
||||||
|
!matches!(provided, Some(WebmAudioCodec::Vorbis)),
|
||||||
|
),
|
||||||
|
_ => (provided, false),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn mp4_audio(
|
||||||
|
allow_audio: bool,
|
||||||
|
has_audio: bool,
|
||||||
|
prescribed: Option<AudioCodec>,
|
||||||
|
provided: Option<Mp4AudioCodec>,
|
||||||
|
) -> (Option<Mp4AudioCodec>, bool) {
|
||||||
|
if allow_audio && has_audio {
|
||||||
|
match prescribed {
|
||||||
|
Some(AudioCodec::Aac) => (
|
||||||
|
Some(Mp4AudioCodec::Aac),
|
||||||
|
!matches!(provided, Some(Mp4AudioCodec::Aac)),
|
||||||
|
),
|
||||||
|
_ => (provided, false),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputVideoFormat {
|
||||||
|
pub(crate) const fn internal_format(self) -> InternalVideoFormat {
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 => "mp4",
|
Self::Mp4 { .. } => InternalVideoFormat::Mp4,
|
||||||
Self::Webm { .. } => "webm",
|
Self::Webm { .. } => InternalVideoFormat::Webm,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn internal_format(self) -> InternalVideoFormat {
|
const fn transcode_vorbis(
|
||||||
|
self,
|
||||||
|
prescribed_codec: WebmAlphaCodec,
|
||||||
|
prescribed_audio_codec: Option<AudioCodec>,
|
||||||
|
allow_audio: bool,
|
||||||
|
) -> OutputVideo {
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 => InternalVideoFormat::Mp4,
|
Self::Webm {
|
||||||
Self::Webm { .. } => InternalVideoFormat::Webm,
|
video_codec,
|
||||||
|
audio_codec,
|
||||||
|
} => {
|
||||||
|
let (audio_codec, transcode_audio) = webm_audio(
|
||||||
|
allow_audio,
|
||||||
|
audio_codec.is_some(),
|
||||||
|
prescribed_audio_codec,
|
||||||
|
audio_codec,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (alpha, transcode_video) = match video_codec {
|
||||||
|
WebmCodec::Alpha(AlphaCodec { alpha, codec }) => {
|
||||||
|
(alpha, !codec.const_eq(prescribed_codec))
|
||||||
|
}
|
||||||
|
WebmCodec::Av1 => (false, true),
|
||||||
|
};
|
||||||
|
|
||||||
|
OutputVideo {
|
||||||
|
format: OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha,
|
||||||
|
codec: prescribed_codec,
|
||||||
|
}),
|
||||||
|
audio_codec,
|
||||||
|
},
|
||||||
|
transcode_video,
|
||||||
|
transcode_audio,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Mp4 { audio_codec, .. } => {
|
||||||
|
let (audio_codec, transcode_audio) = webm_audio(
|
||||||
|
allow_audio,
|
||||||
|
audio_codec.is_some(),
|
||||||
|
prescribed_audio_codec,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
OutputVideo {
|
||||||
|
format: OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: prescribed_codec,
|
||||||
|
}),
|
||||||
|
audio_codec,
|
||||||
|
},
|
||||||
|
transcode_video: true,
|
||||||
|
transcode_audio,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn transcode_av1(
|
||||||
|
self,
|
||||||
|
prescribed_audio_codec: Option<AudioCodec>,
|
||||||
|
allow_audio: bool,
|
||||||
|
) -> OutputVideo {
|
||||||
|
match self {
|
||||||
|
Self::Webm {
|
||||||
|
video_codec,
|
||||||
|
audio_codec,
|
||||||
|
} => {
|
||||||
|
let (audio_codec, transcode_audio) = webm_audio(
|
||||||
|
allow_audio,
|
||||||
|
audio_codec.is_some(),
|
||||||
|
prescribed_audio_codec,
|
||||||
|
audio_codec,
|
||||||
|
);
|
||||||
|
|
||||||
|
OutputVideo {
|
||||||
|
format: OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Av1,
|
||||||
|
audio_codec,
|
||||||
|
},
|
||||||
|
transcode_video: !video_codec.const_eq(WebmCodec::Av1),
|
||||||
|
transcode_audio,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Mp4 { audio_codec, .. } => {
|
||||||
|
let (audio_codec, transcode_audio) = webm_audio(
|
||||||
|
allow_audio,
|
||||||
|
audio_codec.is_some(),
|
||||||
|
prescribed_audio_codec,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
OutputVideo {
|
||||||
|
format: OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Av1,
|
||||||
|
audio_codec,
|
||||||
|
},
|
||||||
|
transcode_video: true,
|
||||||
|
transcode_audio,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn transcode_mp4(
|
||||||
|
self,
|
||||||
|
prescribed_codec: Mp4Codec,
|
||||||
|
prescribed_audio_codec: Option<AudioCodec>,
|
||||||
|
allow_audio: bool,
|
||||||
|
) -> OutputVideo {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 {
|
||||||
|
video_codec,
|
||||||
|
audio_codec,
|
||||||
|
} => {
|
||||||
|
let (audio_codec, transcode_audio) = mp4_audio(
|
||||||
|
allow_audio,
|
||||||
|
audio_codec.is_some(),
|
||||||
|
prescribed_audio_codec,
|
||||||
|
audio_codec,
|
||||||
|
);
|
||||||
|
|
||||||
|
OutputVideo {
|
||||||
|
format: OutputVideoFormat::Mp4 {
|
||||||
|
video_codec: prescribed_codec,
|
||||||
|
audio_codec,
|
||||||
|
},
|
||||||
|
transcode_video: !video_codec.const_eq(prescribed_codec),
|
||||||
|
transcode_audio,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Webm { audio_codec, .. } => {
|
||||||
|
let (audio_codec, transcode_audio) = mp4_audio(
|
||||||
|
allow_audio,
|
||||||
|
audio_codec.is_some(),
|
||||||
|
prescribed_audio_codec,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
OutputVideo {
|
||||||
|
format: OutputVideoFormat::Mp4 {
|
||||||
|
video_codec: prescribed_codec,
|
||||||
|
audio_codec,
|
||||||
|
},
|
||||||
|
transcode_video: true,
|
||||||
|
transcode_audio,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn build_output(
|
pub(crate) const fn build_output(
|
||||||
self,
|
self,
|
||||||
video_codec: VideoCodec,
|
prescribed_video_codec: Option<VideoCodec>,
|
||||||
audio_codec: Option<AudioCodec>,
|
prescribed_audio_codec: Option<AudioCodec>,
|
||||||
allow_audio: bool,
|
allow_audio: bool,
|
||||||
) -> OutputVideoFormat {
|
) -> OutputVideo {
|
||||||
match (video_codec, self) {
|
match prescribed_video_codec {
|
||||||
(VideoCodec::Vp8, Self::Webm { alpha }) => OutputVideoFormat::Webm {
|
Some(VideoCodec::Vp8) => {
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
self.transcode_vorbis(WebmAlphaCodec::Vp8, prescribed_audio_codec, allow_audio)
|
||||||
alpha,
|
}
|
||||||
codec: WebmAlphaCodec::Vp8,
|
Some(VideoCodec::Vp9) => {
|
||||||
}),
|
self.transcode_vorbis(WebmAlphaCodec::Vp9, prescribed_audio_codec, allow_audio)
|
||||||
audio_codec: if allow_audio {
|
}
|
||||||
match audio_codec {
|
Some(VideoCodec::Av1) => self.transcode_av1(prescribed_audio_codec, allow_audio),
|
||||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
Some(VideoCodec::H264) => {
|
||||||
_ => Some(WebmAudioCodec::Opus),
|
self.transcode_mp4(Mp4Codec::H264, prescribed_audio_codec, allow_audio)
|
||||||
}
|
}
|
||||||
} else {
|
Some(VideoCodec::H265) => {
|
||||||
None
|
self.transcode_mp4(Mp4Codec::H265, prescribed_audio_codec, allow_audio)
|
||||||
},
|
}
|
||||||
|
None => OutputVideo {
|
||||||
|
format: self.to_output(),
|
||||||
|
transcode_video: false,
|
||||||
|
transcode_audio: false,
|
||||||
},
|
},
|
||||||
(VideoCodec::Vp8, _) => OutputVideoFormat::Webm {
|
}
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
}
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp8,
|
const fn to_output(self) -> OutputVideoFormat {
|
||||||
}),
|
match self {
|
||||||
audio_codec: if allow_audio {
|
Self::Mp4 {
|
||||||
match audio_codec {
|
video_codec,
|
||||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
audio_codec,
|
||||||
_ => Some(WebmAudioCodec::Opus),
|
} => OutputVideoFormat::Mp4 {
|
||||||
}
|
video_codec,
|
||||||
} else {
|
audio_codec,
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
(VideoCodec::Vp9, Self::Webm { alpha }) => OutputVideoFormat::Webm {
|
Self::Webm {
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
video_codec,
|
||||||
alpha,
|
audio_codec,
|
||||||
codec: WebmAlphaCodec::Vp9,
|
} => OutputVideoFormat::Webm {
|
||||||
}),
|
video_codec,
|
||||||
audio_codec: if allow_audio {
|
audio_codec,
|
||||||
match audio_codec {
|
|
||||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
|
||||||
_ => Some(WebmAudioCodec::Opus),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp9, _) => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: if allow_audio {
|
|
||||||
match audio_codec {
|
|
||||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
|
||||||
_ => Some(WebmAudioCodec::Opus),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
|
||||||
(VideoCodec::Av1, _) => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Av1,
|
|
||||||
audio_codec: if allow_audio {
|
|
||||||
match audio_codec {
|
|
||||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
|
||||||
_ => Some(WebmAudioCodec::Opus),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
|
||||||
(VideoCodec::H264, _) => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H264,
|
|
||||||
audio_codec: if allow_audio {
|
|
||||||
Some(Mp4AudioCodec::Aac)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
|
||||||
(VideoCodec::H265, _) => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H265,
|
|
||||||
audio_codec: if allow_audio {
|
|
||||||
Some(Mp4AudioCodec::Aac)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 { .. } => "mp4",
|
||||||
|
Self::Webm { .. } => "webm",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutputVideoFormat {
|
impl OutputVideoFormat {
|
||||||
|
@ -242,92 +406,6 @@ impl OutputVideoFormat {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn from_parts(
|
|
||||||
video_codec: VideoCodec,
|
|
||||||
audio_codec: Option<AudioCodec>,
|
|
||||||
allow_audio: bool,
|
|
||||||
) -> Self {
|
|
||||||
match (video_codec, audio_codec) {
|
|
||||||
(VideoCodec::Av1, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Av1,
|
|
||||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
|
||||||
},
|
|
||||||
(VideoCodec::Av1, _) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Av1,
|
|
||||||
audio_codec: Some(WebmAudioCodec::Opus),
|
|
||||||
},
|
|
||||||
(VideoCodec::Av1, _) => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Av1,
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
(VideoCodec::H264, _) if allow_audio => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H264,
|
|
||||||
audio_codec: Some(Mp4AudioCodec::Aac),
|
|
||||||
},
|
|
||||||
(VideoCodec::H264, _) => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H264,
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
(VideoCodec::H265, _) if allow_audio => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H265,
|
|
||||||
audio_codec: Some(Mp4AudioCodec::Aac),
|
|
||||||
},
|
|
||||||
(VideoCodec::H265, _) => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H265,
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp8, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp8,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp8, _) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp8,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Opus),
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp8, _) => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp8,
|
|
||||||
}),
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp9, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp9, _) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Opus),
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp9, _) => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn magick_format(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 { .. } => "MP4",
|
|
||||||
Self::Webm { .. } => "WEBM",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Mp4 { .. } => "mp4",
|
Self::Mp4 { .. } => "mp4",
|
||||||
|
@ -372,14 +450,28 @@ impl OutputVideoFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mp4Codec {
|
impl Mp4Codec {
|
||||||
|
const fn const_eq(self, rhs: Self) -> bool {
|
||||||
|
match (self, rhs) {
|
||||||
|
(Self::Av1, Self::Av1) | (Self::H264, Self::H264) | (Self::H265, Self::H265) => true,
|
||||||
|
(Self::Av1, _) | (Self::H264, _) | (Self::H265, _) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fn ffmpeg_codec(self) -> &'static str {
|
const fn ffmpeg_codec(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Av1 => "av1",
|
||||||
Self::H264 => "h264",
|
Self::H264 => "h264",
|
||||||
Self::H265 => "hevc",
|
Self::H265 => "hevc",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AlphaCodec {
|
||||||
|
const fn const_eq(self, rhs: Self) -> bool {
|
||||||
|
self.alpha == rhs.alpha && self.codec.const_eq(rhs.codec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WebmAlphaCodec {
|
impl WebmAlphaCodec {
|
||||||
const fn is_vp9(&self) -> bool {
|
const fn is_vp9(&self) -> bool {
|
||||||
matches!(self, Self::Vp9)
|
matches!(self, Self::Vp9)
|
||||||
|
@ -391,9 +483,24 @@ impl WebmAlphaCodec {
|
||||||
Self::Vp9 => "vp9",
|
Self::Vp9 => "vp9",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn const_eq(self, rhs: Self) -> bool {
|
||||||
|
match (self, rhs) {
|
||||||
|
(Self::Vp8, Self::Vp8) | (Self::Vp9, Self::Vp9) => true,
|
||||||
|
(Self::Vp8, _) | (Self::Vp9, _) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebmCodec {
|
impl WebmCodec {
|
||||||
|
const fn const_eq(self, rhs: Self) -> bool {
|
||||||
|
match (self, rhs) {
|
||||||
|
(Self::Av1, Self::Av1) => true,
|
||||||
|
(Self::Alpha(this), Self::Alpha(rhs)) => this.const_eq(rhs),
|
||||||
|
(Self::Av1, _) | (Self::Alpha(_), _) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fn is_vp9(self) -> bool {
|
const fn is_vp9(self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Av1 => false,
|
Self::Av1 => false,
|
||||||
|
|
|
@ -7,8 +7,8 @@ use crate::{
|
||||||
either::Either,
|
either::Either,
|
||||||
error::Error,
|
error::Error,
|
||||||
formats::{
|
formats::{
|
||||||
AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InternalFormat,
|
AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InputVideoFormat,
|
||||||
OutputVideoFormat, Validations, VideoFormat,
|
InternalFormat, Validations,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
|
@ -71,7 +71,7 @@ pub(crate) async fn validate_bytes(
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frames.unwrap_or(1),
|
frames.unwrap_or(1),
|
||||||
&validations,
|
validations.animation,
|
||||||
timeout,
|
timeout,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -81,7 +81,7 @@ pub(crate) async fn validate_bytes(
|
||||||
InputFile::Video(input) => {
|
InputFile::Video(input) => {
|
||||||
let (format, read) = process_video(
|
let (format, read) = process_video(
|
||||||
bytes,
|
bytes,
|
||||||
*input,
|
input.clone(),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frames.unwrap_or(1),
|
frames.unwrap_or(1),
|
||||||
|
@ -166,47 +166,25 @@ async fn process_animation(
|
||||||
width: u16,
|
width: u16,
|
||||||
height: u16,
|
height: u16,
|
||||||
frames: u32,
|
frames: u32,
|
||||||
validations: &Validations<'_>,
|
validations: &crate::config::Animation,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
|
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
|
||||||
match validate_animation(bytes.len(), width, height, frames, validations.animation) {
|
validate_animation(bytes.len(), width, height, frames, validations)?;
|
||||||
Ok(()) => {
|
|
||||||
let AnimationOutput {
|
|
||||||
format,
|
|
||||||
needs_transcode,
|
|
||||||
} = input.build_output(validations.animation.format);
|
|
||||||
|
|
||||||
let read = if needs_transcode {
|
let AnimationOutput {
|
||||||
let quality = validations.animation.quality_for(format);
|
format,
|
||||||
|
needs_transcode,
|
||||||
|
} = input.build_output(validations.format);
|
||||||
|
|
||||||
Either::left(
|
let read = if needs_transcode {
|
||||||
magick::convert_animation(input, format, quality, timeout, bytes).await?,
|
let quality = validations.quality_for(format);
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Either::right(Either::left(exiftool::clear_metadata_bytes_read(
|
|
||||||
bytes, timeout,
|
|
||||||
)?))
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((InternalFormat::Animation(format), read))
|
Either::left(magick::convert_animation(input, format, quality, timeout, bytes).await?)
|
||||||
}
|
} else {
|
||||||
Err(_) => match validate_video(bytes.len(), width, height, frames, validations.video) {
|
Either::right(exiftool::clear_metadata_bytes_read(bytes, timeout)?)
|
||||||
Ok(()) => {
|
};
|
||||||
let output = OutputVideoFormat::from_parts(
|
|
||||||
validations.video.video_codec,
|
|
||||||
validations.video.audio_codec,
|
|
||||||
validations.video.allow_audio,
|
|
||||||
);
|
|
||||||
|
|
||||||
let read = Either::right(Either::right(
|
Ok((InternalFormat::Animation(format), read))
|
||||||
magick::convert_video(input, output, timeout, bytes).await?,
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok((InternalFormat::Video(output.internal_format()), read))
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_video(
|
fn validate_video(
|
||||||
|
@ -241,7 +219,7 @@ fn validate_video(
|
||||||
#[tracing::instrument(skip(bytes, validations))]
|
#[tracing::instrument(skip(bytes, validations))]
|
||||||
async fn process_video(
|
async fn process_video(
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
input: VideoFormat,
|
input: InputVideoFormat,
|
||||||
width: u16,
|
width: u16,
|
||||||
height: u16,
|
height: u16,
|
||||||
frames: u32,
|
frames: u32,
|
||||||
|
@ -260,5 +238,5 @@ async fn process_video(
|
||||||
|
|
||||||
let read = ffmpeg::transcode_bytes(input, output, crf, timeout, bytes).await?;
|
let read = ffmpeg::transcode_bytes(input, output, crf, timeout, bytes).await?;
|
||||||
|
|
||||||
Ok((InternalFormat::Video(output.internal_format()), read))
|
Ok((InternalFormat::Video(output.format.internal_format()), read))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,13 @@ use tokio::io::AsyncRead;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ffmpeg::FfMpegError,
|
ffmpeg::FfMpegError,
|
||||||
formats::{OutputVideoFormat, VideoFormat},
|
formats::{InputVideoFormat, OutputVideo},
|
||||||
process::Process,
|
process::Process,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) async fn transcode_bytes(
|
pub(super) async fn transcode_bytes(
|
||||||
input_format: VideoFormat,
|
input_format: InputVideoFormat,
|
||||||
output_format: OutputVideoFormat,
|
output_format: OutputVideo,
|
||||||
crf: u8,
|
crf: u8,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
|
@ -57,12 +57,20 @@ pub(super) async fn transcode_bytes(
|
||||||
|
|
||||||
async fn transcode_files(
|
async fn transcode_files(
|
||||||
input_path: &str,
|
input_path: &str,
|
||||||
input_format: VideoFormat,
|
input_format: InputVideoFormat,
|
||||||
output_path: &str,
|
output_path: &str,
|
||||||
output_format: OutputVideoFormat,
|
output_format: OutputVideo,
|
||||||
crf: u8,
|
crf: u8,
|
||||||
timeout: u64,
|
timeout: u64,
|
||||||
) -> Result<(), FfMpegError> {
|
) -> Result<(), FfMpegError> {
|
||||||
|
let crf = crf.to_string();
|
||||||
|
|
||||||
|
let OutputVideo {
|
||||||
|
transcode_video,
|
||||||
|
transcode_audio,
|
||||||
|
format: output_format,
|
||||||
|
} = output_format;
|
||||||
|
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-v",
|
"-v",
|
||||||
|
@ -71,33 +79,38 @@ async fn transcode_files(
|
||||||
input_format.ffmpeg_format(),
|
input_format.ffmpeg_format(),
|
||||||
"-i",
|
"-i",
|
||||||
input_path,
|
input_path,
|
||||||
"-pix_fmt",
|
|
||||||
output_format.pix_fmt(),
|
|
||||||
"-vf",
|
|
||||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if let Some(audio_codec) = output_format.ffmpeg_audio_codec() {
|
if transcode_video {
|
||||||
args.extend(["-c:a", audio_codec]);
|
args.extend([
|
||||||
|
"-pix_fmt",
|
||||||
|
output_format.pix_fmt(),
|
||||||
|
"-vf",
|
||||||
|
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||||
|
"-c:v",
|
||||||
|
output_format.ffmpeg_video_codec(),
|
||||||
|
"-crf",
|
||||||
|
&crf,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if output_format.is_vp9() {
|
||||||
|
args.extend(["-b:v", "0"]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
args.push("-an")
|
args.extend(["-c:v", "copy"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.extend(["-c:v", output_format.ffmpeg_video_codec()]);
|
if transcode_audio {
|
||||||
|
if let Some(audio_codec) = output_format.ffmpeg_audio_codec() {
|
||||||
if output_format.is_vp9() {
|
args.extend(["-c:a", audio_codec]);
|
||||||
args.extend(["-b:v", "0"]);
|
} else {
|
||||||
|
args.push("-an")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.extend(["-c:a", "copy"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let crf = crf.to_string();
|
args.extend(["-f", output_format.ffmpeg_format(), output_path]);
|
||||||
|
|
||||||
args.extend([
|
|
||||||
"-crf",
|
|
||||||
&crf,
|
|
||||||
"-f",
|
|
||||||
output_format.ffmpeg_format(),
|
|
||||||
output_path,
|
|
||||||
]);
|
|
||||||
|
|
||||||
Process::run("ffmpeg", &args, timeout)?.wait().await?;
|
Process::run("ffmpeg", &args, timeout)?.wait().await?;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use actix_web::web::Bytes;
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
formats::{AnimationFormat, ImageFormat, OutputVideoFormat},
|
formats::{AnimationFormat, ImageFormat},
|
||||||
magick::MagickError,
|
magick::MagickError,
|
||||||
process::Process,
|
process::Process,
|
||||||
};
|
};
|
||||||
|
@ -43,23 +43,6 @@ pub(super) async fn convert_animation(
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn convert_video(
|
|
||||||
input: AnimationFormat,
|
|
||||||
output: OutputVideoFormat,
|
|
||||||
timeout: u64,
|
|
||||||
bytes: Bytes,
|
|
||||||
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
|
||||||
convert(
|
|
||||||
input.magick_format(),
|
|
||||||
output.magick_format(),
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
timeout,
|
|
||||||
bytes,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn convert(
|
async fn convert(
|
||||||
input: &'static str,
|
input: &'static str,
|
||||||
output: &'static str,
|
output: &'static str,
|
||||||
|
|
Loading…
Reference in a new issue