1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/// Streaming media-related processing, operated by means of ffmpeg
///
/// 使用该工具,需要先将ffmpeg配置到环境变量中,通过以下命令进行验证ffmpeg环境是否可用
///
/// ```bash
/// # 安装ffmpeg,官网下载地址:https://ffmpeg.org/download.html,让其ffmpeg、ffprobe命令可在全局使用
/// ffmpeg -version
/// ffprobe -version
/// ```
pub struct FfmpegUtil;
use crate::{CmdUtil, Exception, FileUtil};
use chrono::{Duration, NaiveDateTime};
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
/// FFMPEG 的根路径,必须以路径分隔符结尾
static FFMPEG_ROOT_PATH: OnceLock<PathBuf> = OnceLock::new();
impl FfmpegUtil {
/// 设置 FFMPEG 的根路径。
///
/// 该函数只能被调用一次,后续调用会返回 `Err`。
/// 路径会被规范化,确保其存在且以路径分隔符结尾。
///
/// # Arguments
///
/// * `ffmpeg_root_path` - FFMPEG 的根路径字符串。
///
/// # Errors
///
/// 如果路径不存在、不是目录,或者该函数已被调用过,会返回相应的错误。
pub fn set_ffmpeg_root_path(ffmpeg_root_path: &str) -> Result<(), Exception>{
// 1. 提前检查空字符串,返回明确的错误而不是 panic。
if ffmpeg_root_path.is_empty() {
return Err(Exception::ConfigError("FFmpeg root path cannot be empty.".into()));
}
// 2. 将 &str 转换为 PathBuf,便于后续的路径操作。
let mut path = PathBuf::from(ffmpeg_root_path);
// 3. 规范化路径:解析 . 和 ..,并去除多余的分隔符。
// 这会让路径比较和后续拼接更可靠。
path = path.canonicalize()?;
// 4. 确保路径是一个目录。
if !path.is_dir() {
return Err(Exception::ConfigError(format!("FFmpeg root path '{}' is not a directory.", path.display()).into()));
}
// 5. 确保路径以平台相关的路径分隔符结尾。
// PathBuf 的 push 方法会自动处理分隔符,这是最安全的方式。
path.push("");
if !Path::new(ffmpeg_root_path).exists() {
panic!("ffmpeg root path not exists");
}
// 6. 使用 OnceLock 的 set 方法,并返回 Result,而不是使用 expect。
// 这将错误处理的责任交给了调用者,使函数更健壮。
FFMPEG_ROOT_PATH
.set(path.clone())
.map_err(|_| Exception::ConfigError(format!("FFmpeg root path has already been set to '{}'.", path.display())))?;
Ok(())
}
/// 安全地获取 FFMPEG 的根路径。
pub fn try_get_ffmpeg_root_path() -> Option<&'static PathBuf> {
FFMPEG_ROOT_PATH.get()
}
/// 获取视频文件的时长
///
/// 通过调用ffprobe命令行工具来获取指定视频文件的时长信息
///
/// # 参数
/// * `video_path` - 视频文件的路径字符串引用
///
/// # 返回值
/// * `u64` - 视频秒数
/// * `Option<u64>` - 如果成功获取到视频时长则返回Some(时长秒数),否则返回None
pub fn video_seconds(video_path: &str) -> Option<u64> {
// 如果手动设置了ffmpeg的路径,则使用指定的路径,如果没有指定,则任务使用环境变量中
let root = FfmpegUtil::try_get_ffmpeg_root_path();
let command = if let Some(root) = root {
format!("{}{}", FileUtil::absolute_path_str(root.as_path()).unwrap(), "bin/ffprobe")
} else {
"ffprobe".to_string()
};
// 调用ffprobe命令获取视频时长信息
let output = CmdUtil::run_cmd(vec![&command, "-i", video_path, "-show_entries", "format=duration", "-v", "quiet", "-of", "csv=p=0"]);
match output {
Ok(seconds) => {
// 解析命令行输出结果为u64类型
let seconds: f64 = seconds.trim().parse().unwrap();
let seconds = seconds.round() as u64;
Some(seconds)
},
Err(e) => {
println!("{}", e);
None
}
}
}
/// 将秒数转换为SRT字幕文件所需的时间格式字符串
///
/// 该函数将输入的秒数转换为"小时:分钟:秒,毫秒"的格式,
/// 其中毫秒部分固定为000,符合SRT字幕时间戳的格式要求。
///
/// # 参数
/// * `seconds` - 需要转换的秒数
///
/// # 返回值
/// 返回格式为"HH:MM:SS,000"的字符串,其中:
/// - HH: 小时数,2位数字,不足时前面补0
/// - MM: 分钟数,2位数字,不足时前面补0
/// - SS: 秒数,2位数字,不足时前面补0
/// - 000: 毫秒数,固定为000
pub fn srt_timestr(seconds: u64) -> String {
// 整数除法自动向下取整
let hours: u64 = seconds / 3600;
let minutes: u64 = (seconds % 3600) / 60;
let secs: u64 = seconds % 60;
return format!("{:02}:{:02}:{:02},000", hours, minutes, secs)
}
/// 生成SRT字幕文件
///
/// 该函数根据给定的起始时间、持续秒数和文件路径生成SRT格式的字幕文件。
/// 每一秒生成一个字幕条目,包含序号、时间轴和对应的时间文本。
///
/// # 参数
/// * `start_time_text` - 起始时间字符串,格式为"YYYY-MM-DD HH:MM:SS"
/// * `seconds` - 持续时间,以秒为单位
/// * `srt_file_path` - 要生成的SRT文件路径
///
/// # 返回值
/// 返回Result类型,成功时返回Ok(()),失败时返回包含错误信息的Err
///
/// # 错误
/// 当时间解析失败、文件操作失败或写入失败时会返回相应的错误
pub fn generate_srt(
start_time_text: &str,
seconds: u64,
srt_file_path: &str,
) -> Result<(), Exception> {
// 解析起始时间字符串
let start_time = NaiveDateTime::parse_from_str(start_time_text, "%Y-%m-%d %H:%M:%S")?;
// 创建并打开文件(如果文件已存在,则清空文件)
let mut file = OpenOptions::new().write(true).create(true).truncate(true).open(Path::new(srt_file_path))?;
// 生成每一秒的字幕条目
for i in 0..seconds {
// 计算当前条目的起始时间
let current_start_time = start_time + Duration::seconds(i as i64);
// 写入字幕序号
file.write(format!("{}\n", i + 1).as_bytes())?;
// 写入时间轴
file.write(format!("{} --> {}\n", Self::srt_timestr(i), Self::srt_timestr(i + 1)).as_bytes())?;
// 写入时间文本以及字幕条目之间空一行
file.write(format!("{}\n\n", current_start_time.format("%Y-%m-%d %H:%M:%S")).as_bytes())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let seconds = FfmpegUtil::video_seconds("C:/Users/Administrator/Videos/20250806191023_20250806192914.mp4");
let seconds = seconds.unwrap();
println!("{:?}", seconds);
let timestr = FfmpegUtil::srt_timestr(seconds);
println!("{:?}", timestr);
// 示例用法
let _x = FfmpegUtil::generate_srt(
"2023-10-01 12:00:00",
seconds, // 生成10秒的字幕
"output.srt",
).unwrap();
}
}