Files
Format-Converter/src-tauri/src/ffmpeg_installer.rs
2026-02-06 12:39:11 +06:00

369 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use tokio::process::Command;
use tokio_stream::StreamExt;
const FFMPEG_VERSION: &str = "7.1";
#[derive(Debug, Clone)]
pub struct FFmpegInstaller;
impl FFmpegInstaller {
pub fn new() -> Self {
Self
}
/// 获取 FFmpeg 存储路径
pub fn get_ffmpeg_path() -> PathBuf {
let app_dir = Self::get_app_data_dir();
#[cfg(target_os = "windows")]
let ffmpeg_name = "ffmpeg.exe";
#[cfg(not(target_os = "windows"))]
let ffmpeg_name = "ffmpeg";
app_dir.join(ffmpeg_name)
}
/// 获取应用数据目录
fn get_app_data_dir() -> PathBuf {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
#[cfg(target_os = "macos")]
let app_dir = home.join("Library/Application Support/FormatConverter");
#[cfg(target_os = "windows")]
let app_dir = home.join("AppData/Local/FormatConverter");
#[cfg(target_os = "linux")]
let app_dir = home.join(".local/share/format-converter");
fs::create_dir_all(&app_dir).ok();
app_dir
}
/// 检查 FFmpeg 是否已安装
pub fn is_installed() -> bool {
// 1. 检查系统 PATH 中的 FFmpeg
if let Ok(output) = std::process::Command::new("which")
.arg("ffmpeg")
.output()
{
if output.status.success() && !output.stdout.is_empty() {
return true;
}
}
// 2. 检查应用目录
let app_ffmpeg = Self::get_ffmpeg_path();
if app_ffmpeg.exists() {
return true;
}
// 3. 检查系统常见路径
let common_paths = Self::get_system_ffmpeg_paths();
for path in common_paths {
if path.exists() {
return true;
}
}
false
}
/// 获取可能的系统 FFmpeg 路径
fn get_system_ffmpeg_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
#[cfg(target_os = "macos")]
{
// Homebrew 常见安装路径
paths.push(PathBuf::from("/opt/homebrew/bin/ffmpeg")); // Apple Silicon
paths.push(PathBuf::from("/usr/local/bin/ffmpeg")); // Intel
// 其他可能路径
paths.push(PathBuf::from("/usr/bin/ffmpeg"));
}
#[cfg(target_os = "linux")]
{
paths.push(PathBuf::from("/usr/bin/ffmpeg"));
paths.push(PathBuf::from("/usr/local/bin/ffmpeg"));
}
#[cfg(target_os = "windows")]
{
paths.push(PathBuf::from("C:\\ffmpeg\\bin\\ffmpeg.exe"));
if let Ok(program_files) = std::env::var("ProgramFiles") {
paths.push(PathBuf::from(&format!("{}\\ffmpeg\\bin\\ffmpeg.exe", program_files)));
}
}
paths
}
/// 获取 FFmpeg 版本
pub async fn get_version() -> Option<String> {
// 1. 尝试系统 PATH 中的 ffmpeg
if let Ok(output) = tokio::time::timeout(
std::time::Duration::from_secs(5),
Command::new("ffmpeg").args(["-version"]).output()
).await {
if let Ok(output) = output {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(first_line) = stdout.lines().next() {
return Some(first_line.to_string());
}
}
}
}
// 2. 尝试应用目录中的 ffmpeg
let ffmpeg_path = Self::get_ffmpeg_path();
if ffmpeg_path.exists() {
if let Some(cmd_str) = ffmpeg_path.to_str() {
if let Ok(Ok(output)) = tokio::time::timeout(
std::time::Duration::from_secs(5),
Command::new(cmd_str).args(["-version"]).output()
).await {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(first_line) = stdout.lines().next() {
return Some(first_line.to_string());
}
}
}
}
}
// 3. 尝试系统常见路径
let system_paths = Self::get_system_ffmpeg_paths();
for path in system_paths {
if path.exists() {
if let Some(cmd_str) = path.to_str() {
if let Ok(Ok(output)) = tokio::time::timeout(
std::time::Duration::from_secs(5),
Command::new(cmd_str).args(["-version"]).output()
).await {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(first_line) = stdout.lines().next() {
return Some(first_line.to_string());
}
}
}
}
}
}
None
}
/// 下载并安装 FFmpeg
pub async fn install<F>(progress_callback: F) -> Result<(), String>
where
F: Fn(f64, String) + Send + 'static,
{
let platform = Self::detect_platform()?;
let download_url = Self::get_download_url(&platform);
progress_callback(0.0, "准备下载 FFmpeg...".to_string());
// 创建临时目录
let temp_dir = std::env::temp_dir().join("format-converter-ffmpeg");
fs::create_dir_all(&temp_dir).map_err(|e: std::io::Error| format!("创建临时目录失败: {}", e))?;
// 下载文件
progress_callback(5.0, "正在下载 FFmpeg...".to_string());
let archive_path = temp_dir.join(format!("ffmpeg.{}", platform.extension()));
Self::download_file(&download_url, &archive_path, |downloaded, total| {
let percent = if total > 0 {
(downloaded as f64 / total as f64) * 40.0 + 5.0
} else {
5.0
};
progress_callback(percent, format!("正在下载 FFmpeg... {:.1}%", percent));
})
.await
.map_err(|e| format!("下载失败: {}", e))?;
// 解压
progress_callback(50.0, "正在解压...".to_string());
Self::extract_archive(&archive_path, &temp_dir, &platform)
.await
.map_err(|e| format!("解压失败: {}", e))?;
// 移动到目标位置
progress_callback(90.0, "正在安装...".to_string());
let ffmpeg_path = Self::get_ffmpeg_path();
let extracted_ffmpeg = Self::find_ffmpeg_binary(&temp_dir, &platform)
.ok_or("在解压文件中未找到 FFmpeg 可执行文件")?;
fs::copy(&extracted_ffmpeg, &ffmpeg_path)
.map_err(|e| format!("复制文件失败: {}", e))?;
// 设置可执行权限Unix
#[cfg(not(target_os = "windows"))]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&ffmpeg_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&ffmpeg_path, perms).unwrap();
}
// 清理临时文件
progress_callback(95.0, "正在清理...".to_string());
fs::remove_dir_all(&temp_dir).ok();
progress_callback(100.0, "FFmpeg 安装完成".to_string());
Ok(())
}
/// 检测平台
fn detect_platform() -> Result<Platform, String> {
#[cfg(target_os = "macos")]
{
// 检测架构
let arch = std::env::consts::ARCH;
match arch {
"aarch64" => Ok(Platform::MacOSArm64),
"x86_64" => Ok(Platform::MacOSX64),
_ => Err(format!("不支持的架构: {}", arch)),
}
}
#[cfg(target_os = "windows")]
{
let arch = std::env::consts::ARCH;
match arch {
"x86_64" => Ok(Platform::WindowsX64),
"aarch64" => Ok(Platform::WindowsArm64),
_ => Err(format!("不支持的架构: {}", arch)),
}
}
#[cfg(target_os = "linux")]
{
let arch = std::env::consts::ARCH;
match arch {
"x86_64" => Ok(Platform::LinuxX64),
"aarch64" => Ok(Platform::LinuxArm64),
_ => Err(format!("不支持的架构: {}", arch)),
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
Err("不支持的操作系统".to_string())
}
}
/// 获取下载 URL
fn get_download_url(platform: &Platform) -> String {
match platform {
Platform::MacOSArm64 => format!(
"https://evermeet.cx/pub/ffmpeg/ffmpeg-{}-macos-arm64.zip",
FFMPEG_VERSION
),
Platform::MacOSX64 => format!(
"https://evermeet.cx/pub/ffmpeg/ffmpeg-{}-macos-x86_64.zip",
FFMPEG_VERSION
),
Platform::WindowsX64 => format!(
"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
),
Platform::WindowsArm64 => format!(
"https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
),
Platform::LinuxX64 => format!(
"https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"
),
Platform::LinuxArm64 => format!(
"https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz"
),
}
}
/// 下载文件
async fn download_file<F>(url: &str, path: &Path, progress: F) -> Result<(), String>
where
F: Fn(u64, u64),
{
let client = reqwest::Client::new();
let response = client
.get(url)
.timeout(std::time::Duration::from_secs(300))
.send()
.await
.map_err(|e| e.to_string())?;
let total_size = response.content_length().unwrap_or(0);
let mut file = fs::File::create(path).map_err(|e| e.to_string())?;
let mut downloaded: u64 = 0;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e: reqwest::Error| e.to_string())?;
file.write_all(&chunk).map_err(|e: std::io::Error| e.to_string())?;
downloaded += chunk.len() as u64;
progress(downloaded, total_size);
}
Ok(())
}
/// 解压归档文件
async fn extract_archive(
archive_path: &Path,
output_dir: &Path,
platform: &Platform,
) -> Result<(), String> {
match platform.extension().as_str() {
"zip" => {
let file = fs::File::open(archive_path).map_err(|e| e.to_string())?;
let mut archive = zip::ZipArchive::new(file).map_err(|e| e.to_string())?;
archive.extract(output_dir).map_err(|e| e.to_string())?;
}
"tar.xz" => {
let file = fs::File::open(archive_path).map_err(|e| e.to_string())?;
let tar = xz2::read::XzDecoder::new(file);
let mut archive = tar::Archive::new(tar);
archive.unpack(output_dir).map_err(|e| e.to_string())?;
}
_ => return Err("不支持的压缩格式".to_string()),
}
Ok(())
}
/// 查找 FFmpeg 可执行文件
fn find_ffmpeg_binary(dir: &Path, _platform: &Platform) -> Option<PathBuf> {
#[cfg(target_os = "windows")]
let binary_name = "ffmpeg.exe";
#[cfg(not(target_os = "windows"))]
let binary_name = "ffmpeg";
for entry in walkdir::WalkDir::new(dir) {
if let Ok(entry) = entry {
if entry.file_name() == binary_name {
return Some(entry.path().to_path_buf());
}
}
}
None
}
}
#[derive(Debug)]
enum Platform {
MacOSArm64,
MacOSX64,
WindowsX64,
WindowsArm64,
LinuxX64,
LinuxArm64,
}
impl Platform {
fn extension(&self) -> String {
match self {
Platform::MacOSArm64 | Platform::MacOSX64 | Platform::WindowsX64 | Platform::WindowsArm64 => "zip".to_string(),
Platform::LinuxX64 | Platform::LinuxArm64 => "tar.xz".to_string(),
}
}
}