// Copyright (C) 2026 Elias Wendland // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3 exclusively. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use crate::plugin::Plugin; use std::io::Write; use std::path::Path; pub struct PluginImpl; impl Plugin for PluginImpl { fn name(&self) -> &'static str { "ffmpeg_video" } fn from_formats(&self) -> Vec<&'static str> { vec![ "mp4", "webm", "mkv", "avi", "mov", "wmv", "flv", "gif", "m4v", "3gp", "ts", "m2ts", "vob", "mpg", "mpeg", "mxf", "ogv", "apng", "webp", ] } fn to_formats(&self) -> Vec<&'static str> { vec![ "mp4", "webm", "mkv", "avi", "mov", "wmv", "flv", "gif", "m4v", "3gp", "ts", "m2ts", "vob", "mpg", "mpeg", "mxf", "ogv", "apng", "webp", ] } #[tracing::instrument(skip(self))] fn is_available(&self) -> bool { tracing::trace!("Checking availability of ffmpeg for ffmpeg_video plugin"); let available = std::process::Command::new("ffmpeg") .arg("-version") .output() .is_ok(); tracing::debug!("ffmpeg_video plugin available: {}", available); available } fn familiarity(&self, _from: &str, to: &str) -> u8 { match to { "mp4" | "gif" => 255, "mov" | "m4v" => 240, "webm" => 220, "mkv" => 180, "wmv" | "mpg" | "mpeg" => 160, "flv" | "avi" | "3gp" | "vob" => 120, "ts" | "m2ts" | "mxf" | "ogv" | "apng" | "webp" => 100, _ => 128, } } fn quality(&self, _from: &str, to: &str) -> u8 { match to { "mkv" | "mxf" | "ts" | "m2ts" => 250, "mov" | "mp4" | "m4v" => 240, "webm" => 210, "wmv" | "mpg" | "mpeg" | "vob" => 200, "flv" | "avi" | "ogv" | "3gp" => 150, "gif" | "apng" => 100, "webp" => 50, _ => 200, } } fn speed(&self, _from: &str, to: &str) -> u8 { match to { "mp4" | "mov" | "m4v" => 220, "mkv" | "mpg" | "mpeg" | "ts" | "m2ts" | "mxf" | "vob" => 200, "flv" | "avi" | "3gp" | "ogv" => 180, "wmv" => 170, "gif" | "apng" => 150, "webp" => 100, "webm" => 50, _ => 150, } } #[tracing::instrument(skip(self, input, temp_dir))] fn convert( &self, input: &[u8], _from: &str, to: &str, temp_dir: &Path, ) -> Result, String> { tracing::debug!("ffmpeg_video starting conversion: {} -> {}", _from, to); let mut temp_in = tempfile::Builder::new() .suffix(&format!(".{}", _from)) .tempfile_in(temp_dir) .map_err(|e| { tracing::error!("Failed to create temp input file: {}", e); e.to_string() })?; temp_in.write_all(input).map_err(|e| { tracing::error!("Failed to write to temp input file: {}", e); e.to_string() })?; let temp_out = tempfile::Builder::new() .suffix(&format!(".{}", to)) .tempfile_in(temp_dir) .map_err(|e| { tracing::error!("Failed to create temp output file: {}", e); e.to_string() })?; let temp_out_path = temp_out.into_temp_path(); let in_path = temp_in.path().to_path_buf(); tracing::trace!( "Temp files created. In: {:?}, Out: {:?}", in_path, temp_out_path ); let mut raw_args = vec![]; match to { "mp4" | "mkv" | "mov" | "m4v" => { raw_args.extend(vec![ "-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-pix_fmt", "yuv420p", ]); } "webm" => { raw_args.extend(vec![ "-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-c:a", "libopus", ]); } "avi" => { raw_args.extend(vec![ "-c:v", "mpeg4", "-vtag", "xvid", "-qscale:v", "3", "-c:a", "libmp3lame", ]); } "wmv" => { raw_args.extend(vec![ "-c:v", "wmv2", "-b:v", "1024k", "-c:a", "wmav2", "-b:a", "128k", ]); } "flv" => { raw_args.extend(vec![ "-c:v", "flv1", "-b:v", "800k", "-c:a", "libmp3lame", "-b:a", "128k", ]); } "ts" | "m2ts" | "mpg" | "mpeg" | "vob" => { raw_args.extend(vec![ "-c:v", "mpeg2video", "-qscale:v", "2", "-c:a", "mp2", "-b:a", "192k", ]); } "mxf" => { raw_args.extend(vec![ "-c:v", "mpeg2video", "-qscale:v", "2", "-c:a", "pcm_s16le", ]); } "ogv" => { raw_args.extend(vec![ "-c:v", "libtheora", "-qscale:v", "6", "-c:a", "libvorbis", "-qscale:a", "4", ]); } "3gp" => { raw_args.extend(vec![ "-c:v", "h263", "-s", "352x288", "-c:a", "libopencore_amrnb", "-ar", "8000", ]); } "gif" => { raw_args.extend(vec!["-vf", "fps=15,scale=320:-1:flags=lanczos"]); } "webp" => { raw_args.extend(vec![ "-c:v", "libwebp", "-lossless", "0", "-qscale", "75", "-loop", "0", ]); } "apng" => { raw_args.extend(vec!["-plays", "0"]); } _ => { raw_args.extend(vec!["-qscale:v", "3"]); } } tracing::debug!("Built ffmpeg arguments: {:?}", raw_args); tracing::trace!("Executing ffmpeg command..."); let output = std::process::Command::new("ffmpeg") .arg("-y") // Overwrite output files without asking .arg("-i") .arg(&in_path) .args(&raw_args) .arg(&temp_out_path) .output() .map_err(|e| { tracing::error!("Failed to execute ffmpeg: {}", e); e.to_string() })?; if !output.status.success() { let err = String::from_utf8_lossy(&output.stderr); tracing::error!("ffmpeg execution failed: {}", err); return Err(format!("ffmpeg failed: {}", err)); } tracing::debug!("FFmpeg execution succeeded. Reading output file..."); std::fs::read(&temp_out_path).map_err(|e| { tracing::error!("Failed to read output file {:?}: {}", temp_out_path, e); e.to_string() }) } }