Video Gifs #34
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1423,6 +1423,7 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ prometheus = "0.13"
|
|||||||
lazy_static = "1.5"
|
lazy_static = "1.5"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
tempfile = "3.14.0"
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ use crate::files::{
|
|||||||
use crate::service::ServiceBuilder;
|
use crate::service::ServiceBuilder;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::tags::*;
|
use crate::tags::*;
|
||||||
use crate::video::*;
|
|
||||||
use crate::video::actors::{
|
use crate::video::actors::{
|
||||||
create_playlist, generate_video_thumbnail, ProcessMessage, ScanDirectoryMessage,
|
create_playlist, generate_video_thumbnail, ProcessMessage, ScanDirectoryMessage,
|
||||||
};
|
};
|
||||||
|
|||||||
136
src/video/ffmpeg.rs
Normal file
136
src/video/ffmpeg.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
use futures::future::{Inspect, MapOk};
|
||||||
|
use futures::task::SpawnExt;
|
||||||
|
use futures::{FutureExt, TryFutureExt};
|
||||||
|
use log::{debug, error, info, trace, warn};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::io::{Result, Stdout};
|
||||||
|
use std::process::{Output, Stdio};
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
pub struct Ffmpeg;
|
||||||
|
|
||||||
|
impl Ffmpeg {
|
||||||
|
async fn generate_playlist(&self, input_file: &str, output_file: &str) -> Result<String> {
|
||||||
|
let ffmpeg_result: Result<Output> = tokio::process::Command::new("ffmpeg")
|
||||||
|
.arg("-i")
|
||||||
|
.arg(input_file)
|
||||||
|
.arg("-c:v")
|
||||||
|
.arg("h264")
|
||||||
|
.arg("-crf")
|
||||||
|
.arg("21")
|
||||||
|
.arg("-preset")
|
||||||
|
.arg("veryfast")
|
||||||
|
.arg("-hls_time")
|
||||||
|
.arg("3")
|
||||||
|
.arg("-hls_list_size")
|
||||||
|
.arg("100")
|
||||||
|
.arg("-vf")
|
||||||
|
.arg("scale=1080:-2,setsar=1:1")
|
||||||
|
.arg(output_file)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.inspect_err(|e| error!("Failed to run ffmpeg on child process: {}", e))
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(ref res) = ffmpeg_result {
|
||||||
|
debug!("ffmpeg output: {:?}", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpeg_result.map(|_| output_file.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_video_duration(&self, input_file: &str) -> Result<String> {
|
||||||
|
Command::new("ffprobe")
|
||||||
|
.args(&["-i", input_file])
|
||||||
|
.args(&["-show_entries", "format=duration"])
|
||||||
|
.args(&["-v", "quiet"])
|
||||||
|
.args(&["-of", "csv=p=0"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||||
|
.inspect(|duration| debug!("Found video duration: {:?}", duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_video_gif(
|
||||||
|
&self,
|
||||||
|
input_file: &str,
|
||||||
|
output_file: &str,
|
||||||
|
duration_sec: u32,
|
||||||
|
) -> Result<String> {
|
||||||
|
info!("Creating gif for: '{}'", input_file);
|
||||||
|
|
||||||
|
let temp_dir = tempfile::tempdir()?;
|
||||||
|
let temp_path = temp_dir
|
||||||
|
.path()
|
||||||
|
.to_str()
|
||||||
|
.expect("Unable to make temp_dir a string");
|
||||||
|
|
||||||
|
match self
|
||||||
|
.get_video_duration(input_file)
|
||||||
|
.and_then(|duration| {
|
||||||
|
debug!("Creating gif frames for '{}'", input_file);
|
||||||
|
|
||||||
|
Command::new("ffmpeg")
|
||||||
|
.args(&["-i", input_file])
|
||||||
|
.args(&["-vf", &format!("fps=20/{}", duration)])
|
||||||
|
.args(&["-q:v", "2"])
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.arg(format!("{}/frame_%03d.jpg", temp_path))
|
||||||
|
.status()
|
||||||
|
})
|
||||||
|
.and_then(|_| {
|
||||||
|
debug!("Generating palette");
|
||||||
|
|
||||||
|
Command::new("ffmpeg")
|
||||||
|
.args(&["-i", &format!("{}/frame_%03d.jpg", temp_path)])
|
||||||
|
.args(&["-vf", "palettegen"])
|
||||||
|
.arg(&format!("{}/palette.png", temp_path))
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
})
|
||||||
|
.and_then(|_| {
|
||||||
|
debug!("Creating gif for: '{}'", input_file);
|
||||||
|
self.create_gif_from_frames(temp_path, output_file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(exit_code) => {
|
||||||
|
if exit_code == 0 {
|
||||||
|
info!("Created gif for '{}' -> '{}'", input_file, output_file);
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Failed to create gif for '{}' with exit code: {}",
|
||||||
|
input_file, exit_code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error creating gif for '{}': {:?}", input_file, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output_file.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_gif_from_frames(&self, frame_base_dir: &str, output_file: &str) -> Result<i32> {
|
||||||
|
Command::new("ffmpeg")
|
||||||
|
.arg("-y")
|
||||||
|
.args(&["-framerate", "4"])
|
||||||
|
.args(&["-i", &format!("{}/frame_%03d.jpg", frame_base_dir)])
|
||||||
|
.args(&["-i", &format!("{}/palette.png", frame_base_dir)])
|
||||||
|
.args(&[
|
||||||
|
"-filter_complex",
|
||||||
|
// Scale to 480x480 with a center crop
|
||||||
|
"[0:v]scale=480:-1:flags=lanczos,crop='min(in_w,in_h)':'min(in_w,in_h)':(in_w-out_w)/2:(in_h-out_h)/2, paletteuse",
|
||||||
|
])
|
||||||
|
.args(&["-loop", "0"]) // loop forever
|
||||||
|
.arg(output_file)
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
.map_ok(|out| out.code().unwrap_or( -1))
|
||||||
|
.inspect_ok(|output| debug!("ffmpeg gif create exit code: {:?}", output))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/video/mod.rs
Normal file
2
src/video/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod actors;
|
||||||
|
pub mod ffmpeg;
|
||||||
Reference in New Issue
Block a user