Skip to content

Commit 7d00024

Browse files
committed
[+] release 0.6.0
1 parent 3bfd095 commit 7d00024

36 files changed

+5815
-168
lines changed

lib/video-utils/PROGRESS.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Video Utils 实现进度总结
2+
3+
## 日期: 2026-01-28
4+
5+
### 已完成功能 (3/20)
6+
7+
#### 1. ✅ 视频修剪/裁剪 (Trim/Cut)
8+
**文件**: `lib/video-utils/src/editor/trim.rs`
9+
10+
**功能**:
11+
- 从视频中提取指定时间片段
12+
- 支持精确的时间范围控制
13+
- 使用现有的帧提取和编码器基础设施
14+
15+
**API**:
16+
```rust
17+
// 方法1: 使用 TrimConfig
18+
let config = TrimConfig::new("input.mp4", "output.mp4", Duration::from_secs(10))
19+
.with_end(Duration::from_secs(30));
20+
trim_video(config)?;
21+
22+
// 方法2: 便捷函数
23+
extract_segment("input.mp4", "output.mp4", 10.0, 20.0)?;
24+
```
25+
26+
**示例**: `examples/trim_demo.rs`
27+
- 测试1: 提取前2秒
28+
- 测试2: 提取1-3秒片段
29+
- 测试3: 从2秒到结尾
30+
31+
**限制**:
32+
- 当前仅支持视频,音频支持需要增强 `AudioSamples` 结构体
33+
34+
---
35+
36+
#### 2. ✅ 视频拼接/合并 (Concatenate/Merge)
37+
**文件**: `lib/video-utils/src/editor/concat.rs`
38+
39+
**功能**:
40+
- 将多个视频首尾相连
41+
- 自动分辨率归一化(缩放以匹配目标尺寸)
42+
- 简单的双线性插值缩放
43+
44+
**API**:
45+
```rust
46+
// 方法1: 使用 ConcatConfig
47+
let config = ConcatConfig::new(
48+
vec!["clip1.mp4".into(), "clip2.mp4".into()],
49+
"output.mp4".into(),
50+
)
51+
.with_resolution(1920, 1080);
52+
53+
concat_videos(config)?;
54+
55+
// 方法2: 便捷函数
56+
concat_videos_simple(
57+
vec!["clip1.mp4".into(), "clip2.mp4".into()],
58+
"output.mp4"
59+
)?;
60+
```
61+
62+
**示例**: `examples/concat_demo.rs`
63+
- 测试1: 简单拼接3个视频
64+
- 测试2: 拼接并归一化到1280x720
65+
66+
**限制**:
67+
- 需要音频支持增强
68+
69+
---
70+
71+
#### 3. ✅ 视频缩放/调整尺寸 (Scale/Resize)
72+
**文件**: `lib/video-utils/src/filters/scale.rs`
73+
74+
**功能**:
75+
- 改变视频分辨率
76+
- 3种质量算法:
77+
- Fast: 最近邻插值
78+
- Medium: 双线性插值
79+
- High/Best: 双三次插值
80+
- 自动宽高比保持
81+
- 精确尺寸或适配尺寸模式
82+
83+
**API**:
84+
```rust
85+
// 精确尺寸
86+
let config = ScaleConfig::new("input.mp4", "output.mp4", 1280, 720)
87+
.with_quality(ScaleQuality::High);
88+
scale_video(config)?;
89+
90+
// 适配尺寸(保持宽高比)
91+
scale_to_fit("input.mp4", "output.mp4", 1920, 1080)?;
92+
93+
// 强制尺寸(可能拉伸)
94+
scale_to_exact("input.mp4", "output.mp4", 640, 480)?;
95+
```
96+
97+
**示例**: `examples/scale_demo.rs`
98+
- 测试1: 缩放到720p (保持宽高比)
99+
- 测试2: 适配640x480
100+
- 测试3: 强制320x240
101+
- 测试4: 快速缩放
102+
103+
**算法实现**:
104+
- `scale_nearest_neighbor()` - 快速但质量较低
105+
- `scale_bilinear()` - 平衡质量和速度
106+
- `scale_bicubic()` - 高质量(当前使用双线性作为简化实现)
107+
108+
---
109+
110+
## 验证方法
111+
112+
所有示例都使用 `ffprobe` 验证输出:
113+
114+
```bash
115+
# 运行示例
116+
cargo run --example scale_demo --features ffmpeg
117+
cargo run --example trim_demo --features ffmpeg
118+
cargo run --example concat_demo --features ffmpeg
119+
120+
# 手动验证
121+
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration -of default=noprint_wrappers=1:nokey=1 output.mp4
122+
```
123+
124+
---
125+
126+
## 代码结构
127+
128+
```
129+
lib/video-utils/src/
130+
├── editor/
131+
│ ├── mod.rs # 模块导出
132+
│ ├── trim.rs # ✅ 视频修剪
133+
│ └── concat.rs # ✅ 视频拼接
134+
├── filters/
135+
│ ├── mod.rs # 模块导出
136+
│ ├── scale.rs # ✅ 视频缩放
137+
│ ├── transform.rs # (stub) 旋转/翻转
138+
│ └── fade.rs # (stub) 淡入淡出
139+
└── examples/
140+
├── scale_demo.rs # ✅ 缩放示例
141+
├── trim_demo.rs # ✅ 修剪示例
142+
└── concat_demo.rs # ✅ 拼接示例
143+
```
144+
145+
---
146+
147+
## 下一步 (按优先级)
148+
149+
### Priority 1 剩余功能
150+
4. **视频分割** - 在指定时间点分割视频
151+
5. **音频裁剪** - 提取音频片段到文件
152+
6. **音频混音** - 合并多个音频轨道
153+
7. **音频替换** - 替换视频的音频轨道
154+
8. **速度控制** - 加速/减速视频
155+
156+
### Priority 2 (滤镜)
157+
9. **裁剪** - 提取矩形区域
158+
10. **旋转/翻转** - 几何变换
159+
11. **淡入淡出** - 透明度渐变
160+
12. **交叉淡化** - 视频间过渡
161+
13. **文本叠加** - 添加标题/水印
162+
14. **图像叠加** - 画中画
163+
15. **颜色调整** - 亮度/对比度/饱和度
164+
165+
---
166+
167+
## 技术亮点
168+
169+
1. **重用现有基础设施**: 所有功能都使用现有的 `MP4Encoder` 和帧提取功能
170+
2. **类型安全**: 使用 `Duration` 而不是 `f64` 表示时间
171+
3. **错误处理**: 统一的 `Result<T>` 类型
172+
4. **验证**: 每个功能都有示例和 ffprobe 验证
173+
5. **算法实现**: 手工实现了3种缩放算法
174+
175+
---
176+
177+
## 已知限制
178+
179+
1. **音频支持**: `AudioSamples` 结构体需要增强以包含实际样本数据
180+
2. **性能**: 当前基于帧提取的方法对长视频可能较慢
181+
3. **内存**: 整个视频的所有帧会被加载到内存中
182+
183+
---
184+
185+
## 统计
186+
187+
- **代码行数**: ~1500 行新增代码
188+
- **测试示例**: 3 个完整示例程序
189+
- **编译状态**: ✅ 通过(仅7个警告)
190+
- **覆盖率**: 15% (3/20 功能)
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
//! Video color adjustment example
2+
//!
3+
//! This example demonstrates adjusting video brightness, contrast, and saturation.
4+
5+
use std::path::Path;
6+
use video_utils::filters::color::{
7+
adjust_color, ColorAdjustConfig, adjust_brightness, adjust_contrast, adjust_saturation,
8+
};
9+
use video_utils::metadata::get_metadata;
10+
11+
fn main() -> Result<(), Box<dyn std::error::Error>> {
12+
env_logger::init();
13+
14+
println!("╔════════════════════════════════════════════════════════════════╗");
15+
println!("║ 视频颜色调整功能测试 ║");
16+
println!("╚════════════════════════════════════════════════════════════════╝");
17+
println!();
18+
19+
// Check if test file exists
20+
let input_file = "data/test.mp4";
21+
if !Path::new(input_file).exists() {
22+
println!("❌ 测试文件不存在: {}", input_file);
23+
println!("请先确保有测试视频文件");
24+
return Ok(());
25+
}
26+
27+
// Get original metadata
28+
println!("📹 原始视频信息:");
29+
let metadata = get_metadata(input_file)?;
30+
println!(" 时长: {:.2} 秒", metadata.duration);
31+
println!(" 视频流数: {}", metadata.video_streams_count);
32+
println!();
33+
34+
// Test 1: Increase brightness
35+
println!("【测试1】增加亮度 (+30%)");
36+
println!("=========================================");
37+
38+
match adjust_brightness(input_file, "tmp/color_bright.mp4", 30) {
39+
Ok(_) => println!("✓ 亮度调整完成"),
40+
Err(e) => println!("❌ 调整失败: {}", e),
41+
}
42+
43+
verify_output("tmp/color_bright.mp4", "亮度调整")?;
44+
println!();
45+
46+
// Test 2: Decrease brightness
47+
println!("【测试2】降低亮度 (-30%)");
48+
println!("=========================================");
49+
50+
match adjust_brightness(input_file, "tmp/color_dark.mp4", -30) {
51+
Ok(_) => println!("✓ 亮度调整完成"),
52+
Err(e) => println!("❌ 调整失败: {}", e),
53+
}
54+
55+
verify_output("tmp/color_dark.mp4", "降低亮度")?;
56+
println!();
57+
58+
// Test 3: Increase contrast
59+
println!("【测试3】增加对比度 (+40%)");
60+
println!("=========================================");
61+
62+
match adjust_contrast(input_file, "tmp/color_contrast.mp4", 40) {
63+
Ok(_) => println!("✓ 对比度调整完成"),
64+
Err(e) => println!("❌ 调整失败: {}", e),
65+
}
66+
67+
verify_output("tmp/color_contrast.mp4", "对比度调整")?;
68+
println!();
69+
70+
// Test 4: Grayscale (saturation -100)
71+
println!("【测试4】灰度化 (饱和度 -100%)");
72+
println!("=========================================");
73+
74+
match adjust_saturation(input_file, "tmp/color_gray.mp4", -100) {
75+
Ok(_) => println!("✓ 饱和度调整完成"),
76+
Err(e) => println!("❌ 调整失败: {}", e),
77+
}
78+
79+
verify_output("tmp/color_gray.mp4", "灰度化")?;
80+
println!();
81+
82+
// Test 5: Combined adjustments
83+
println!("【测试5】组合调整 (亮度+20, 对比度+30, 饱和度+50)");
84+
println!("=========================================");
85+
86+
let config = ColorAdjustConfig::new(input_file, "tmp/color_combined.mp4")
87+
.with_brightness(20)
88+
.with_contrast(30)
89+
.with_saturation(50);
90+
91+
match adjust_color(config) {
92+
Ok(_) => println!("✓ 组合调整完成"),
93+
Err(e) => println!("❌ 调整失败: {}", e),
94+
}
95+
96+
verify_output("tmp/color_combined.mp4", "组合调整")?;
97+
println!();
98+
99+
println!("╔════════════════════════════════════════════════════════════════╗");
100+
println!("║ 测试完成 ║");
101+
println!("╚════════════════════════════════════════════════════════════════╝");
102+
103+
Ok(())
104+
}
105+
106+
/// Verify output file using ffprobe
107+
fn verify_output(file: &str, test_name: &str) -> Result<(), Box<dyn std::error::Error>> {
108+
if !Path::new(file).exists() {
109+
println!(" ⚠️ 输出文件不存在: {}", file);
110+
return Ok(());
111+
}
112+
113+
println!(" 🔍 验证输出文件...");
114+
115+
// Use ffprobe to get video info
116+
let output = std::process::Command::new("ffprobe")
117+
.arg("-v")
118+
.arg("error")
119+
.arg("-select_streams")
120+
.arg("v:0")
121+
.arg("-show_entries")
122+
.arg("stream=width,height,duration")
123+
.arg("-of")
124+
.arg("default=noprint_wrappers=1:nokey=1")
125+
.arg(file)
126+
.output()?;
127+
128+
if output.status.success() {
129+
let info = String::from_utf8_lossy(&output.stdout);
130+
let lines: Vec<&str> = info.trim().split('\n').collect();
131+
132+
println!(" ✅ {} 验证通过:", test_name);
133+
for line in lines.iter().take(3) {
134+
let label = match line.trim() {
135+
l if l.parse::<f32>().is_ok() => "时长",
136+
l if l.parse::<u32>().is_ok() && l.parse::<u32>().ok().unwrap_or(5000) < 5000 => "宽度/高度",
137+
_ => line,
138+
};
139+
println!(" {}: {}", label, line.trim());
140+
}
141+
142+
// Get file size
143+
if let Ok(metadata) = std::fs::metadata(file) {
144+
let size_kb = metadata.len() / 1024;
145+
println!(" 大小: {} KB", size_kb);
146+
}
147+
} else {
148+
println!(" ⚠️ ffprobe 验证失败");
149+
let stderr = String::from_utf8_lossy(&output.stderr);
150+
println!(" 错误: {}", stderr);
151+
}
152+
153+
Ok(())
154+
}

0 commit comments

Comments
 (0)