video_util.dart
11.2 KB
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
import 'dart:io';
import 'dart:async';
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:ffmpeg_kit_flutter_new/statistics.dart';
class VideoUtil {
///
/// 将视频格式转换为mp4
/// 转码的同时,根据文件大小自动选择压缩参数
/// iOS 18.6.x: libx264 + 按文件大小自动CRF(h264_videotoolbox在iOS 18.6.x存在稳定性问题)
/// 其它iOS版本: h264_videotoolbox 硬件加速 + 按文件大小自动码率
/// Android: libx264 + 按文件大小自动CRF
/// [onProgress] 进度回调,值范围 0.0 ~ 1.0
///
static Future<bool> convertToMp4(
String inputPath,
String outputPath, {
void Function(int progress)? onProgress,
}) async {
// 先获取视频总时长(微秒)
final duration = await _getVideoDuration(inputPath);
final fileSize = await File(inputPath).length();
String cmd;
if (Platform.isIOS /*&& !_isIos18_6()*/) {
final bitrate = _getBitrateByFileSize(fileSize);
// cmd = '-i "$inputPath" '
// '-c:v h264_videotoolbox ' // 启用 iOS 硬件加速
// '-b:v ${bitrate}k ' // 根据文件大小自动计算码率
// '-vf scale=1280:-2 ' // 缩放到 720p (保持比例)
// '-c:a aac ' // 音频转为 AAC (兼容性最好)
// '-b:a 128k ' // 音频码率
// '"$outputPath"';
cmd = '-i "$inputPath" '
'-c:v h264_videotoolbox '
'-b:v ${bitrate}k '
'-profile:v high ' // 确保使用高压缩率 Profile
'-vf "scale=trunc(oh*a/2)*2:720,format=yuv420p" ' // 强制 720p 且确保像素格式为 yuv420p
'-c:a aac '
'-b:a 128k '
'-movflags +faststart ' // 关键:优化 MP4 结构,支持边下边播
'-y ' // 自动覆盖已存在文件
'"$outputPath"';
} else {
final crf = _getCrfByFileSize(fileSize);
cmd = '-i "$inputPath" '
'-c:v libx264 ' // 设置视频编码器为libx264(H.264)
'-crf $crf ' // 根据文件大小自动计算CRF
'-c:a aac ' // 设置音频编码器为AAC
'-b:a 128k ' // 设置音频比特率为128kbps
'-preset fast '
'-threads 0 '
'-strict experimental ' // 允许使用实验性编解码器功能
'-movflags faststart ' // 优化MP4文件结构
'-f mp4 ' // 指定输出格式为MP4
'"$outputPath"'; // 指定输出文件路径
}
final completer = Completer<bool>();
FFmpegKit.executeAsync(
cmd,
(session) async {
final returnCode = await session.getReturnCode();
completer.complete(ReturnCode.isSuccess(returnCode));
},
null,
(Statistics statistics) {
if (onProgress != null && duration > 0) {
final currentTime = statistics.getTime();
final progress = (currentTime / duration).clamp(0.0, 1.0);
onProgress((progress * 100).floor());
}
},
);
return completer.future;
}
///
/// 通过 ffmpeg 压缩视频
/// [quality] 压缩质量,可选值: 'low' | 'middle' | 'high'
/// 未指定时根据文件大小自动计算压缩参数:文件越大压缩率越高,文件越小清晰度越高
/// iOS 18.6.x: libx264 + CRF(h264_videotoolbox在iOS 18.6.x存在稳定性问题)
/// 其它iOS版本: h264_videotoolbox 硬件加速 + 码率控制
/// Android: libx264 + CRF
/// [onProgress] 进度回调,值范围 0.0 ~ 1.0
///
static Future<bool> compressVideo(
String inputPath,
String outputPath,
String? quality, {
void Function(int progress)? onProgress,
}) async {
final duration = await _getVideoDuration(inputPath);
String cmd;
if (Platform.isIOS /*&& !_isIos18_6()*/) {
// iOS(非18.6+): 使用 h264_videotoolbox 硬件加速
// h264_videotoolbox 不支持 CRF,使用码率控制质量
int bitrate;
if (quality != null && quality.isNotEmpty) {
switch (quality) {
case 'low':
bitrate = 1500;
break;
case 'middle':
bitrate = 2500;
break;
case 'high':
bitrate = 4000;
break;
default:
throw Exception('参数错误');
}
} else {
final fileSize = await File(inputPath).length();
bitrate = _getBitrateByFileSize(fileSize);
}
// cmd = '-i "$inputPath" ' // 输入文件
// '-c:v h264_videotoolbox ' // iOS硬件加速视频编码器
// '-b:v ${bitrate}k ' // 根据质量/文件大小自动计算码率
// '-c:a aac ' // 音频编码器
// '-b:a 128k ' // 音频码率
// '-movflags faststart ' // 优化MP4文件结构
// '"$outputPath"'; // 输出文件
cmd = '-i "$inputPath" '
'-c:v h264_videotoolbox '
'-b:v ${bitrate}k '
'-profile:v high ' // 使用高配置以获得更好画质
'-pix_fmt yuv420p ' // 确保像素格式兼容性
'-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" ' // 强制偶数分辨率,防止报错
'-c:a aac '
'-b:a 128k '
'-movflags +faststart ' // 允许边下边播
'"$outputPath"';
} else {
// iOS 18.6+ / Android: 使用 libx264 软件编码
// 使用CRF模式进行压缩,值范围0-51,建议值18-28
// 高质量: CRF 18-20
// 中等质量: CRF 23-26
// 低质量: CRF 28-32
int crf;
if (quality != null && quality.isNotEmpty) {
switch (quality) {
case 'low':
crf = 32;
break;
case 'middle':
crf = 26;
break;
case 'high':
crf = 20;
break;
default:
throw Exception('参数错误');
}
} else {
final fileSize = await File(inputPath).length();
crf = _getCrfByFileSize(fileSize);
}
cmd = '-i "$inputPath" ' // 输入文件
'-c:v libx264 ' // 视频编码器
'-crf $crf ' // 恒定速率因子(质量控制)
'-c:a aac ' // 音频编码器
'-b:a 128k ' // 音频比特率
// '-preset medium ' // 编码预设
'-preset fast ' // 编码预设,编码速度显著提升,体积损失很小
'-threads 0 ' // 让 libx264 自动使用所有可用 CPU 核心,默认行为可能只用单核
'-movflags faststart ' // 优化MP4文件结构
'"$outputPath"'; // 输出文件
}
final completer = Completer<bool>();
FFmpegKit.executeAsync(
cmd,
(session) async {
final returnCode = await session.getReturnCode();
completer.complete(ReturnCode.isSuccess(returnCode));
},
null,
(Statistics statistics) {
if (onProgress != null && duration > 0) {
final currentTime = statistics.getTime();
final progress = (currentTime / duration).clamp(0.0, 1.0);
onProgress((progress * 100).floor());
}
},
);
return completer.future;
}
/// 判断当前iOS系统版本是否为18.6.x
/// iOS 18.6.x中h264_videotoolbox硬件编码器存在稳定性问题,需改用libx264
static bool _isIos18_6() {
if (!Platform.isIOS) return false;
try {
final versionStr = Platform.operatingSystemVersion;
final regex = RegExp(r'(\d+)\.(\d+)');
final match = regex.firstMatch(versionStr);
if (match != null) {
final major = int.parse(match.group(1)!);
final minor = int.parse(match.group(2)!);
return major == 18 && minor == 6;
}
} catch (_) {}
return false;
}
/// 根据文件大小自动计算视频码率(kbps)
/// 用于iOS h264_videotoolbox硬件加速模式(不支持CRF,需用码率控制质量)
/// 文件越大码率越低(压缩率越高,清晰度越低)
/// 文件越小码率越高(压缩率越低,清晰度越高)
static int _getBitrateByFileSize(int fileSizeBytes) {
final fileSizeMB = fileSizeBytes / (1024 * 1024);
if (fileSizeMB < 5) {
return 4000; // 小文件:高质量
} else if (fileSizeMB < 20) {
return 2500; // 中小文件:较高质量
} else if (fileSizeMB < 50) {
return 1500; // 中等文件:中等质量
} else if (fileSizeMB < 100) {
return 1000; // 较大文件:较低质量
} else if (fileSizeMB < 200) {
return 800; // 大文件:低质量
} else {
return 600; // 超大文件:最低质量
}
}
/// 根据文件大小自动计算CRF值
/// 文件越大CRF越高(压缩率越高,清晰度越低)
/// 文件越小CRF越低(压缩率越低,清晰度越高)
static int _getCrfByFileSize(int fileSizeBytes) {
final fileSizeMB = fileSizeBytes / (1024 * 1024);
if (fileSizeMB < 5) {
return 20; // 小文件:高质量
} else if (fileSizeMB < 20) {
return 23; // 中小文件:较高质量
} else if (fileSizeMB < 50) {
return 26; // 中等文件:中等质量
} else if (fileSizeMB < 100) {
return 28; // 较大文件:较低质量
} else if (fileSizeMB < 200) {
return 30; // 大文件:低质量
} else {
return 32; // 超大文件:最低质量
}
}
/// 获取视频总时长,返回毫秒
static Future<int> _getVideoDuration(String videoPath) async {
final session = await FFmpegKit.execute(
'-i "$videoPath"',
);
final output = await session.getOutput();
// 从 ffmpeg 输出中解析时长,格式如: Duration: 00:01:23.45
final regex = RegExp(r'Duration:\s*(\d+):(\d+):(\d+)\.(\d+)');
final match = regex.firstMatch(output ?? '');
if (match != null) {
final hours = int.parse(match.group(1)!);
final minutes = int.parse(match.group(2)!);
final seconds = int.parse(match.group(3)!);
final ms = int.parse(match.group(4)!);
return (hours * 3600 + minutes * 60 + seconds) * 1000 + ms * 10;
}
return 0;
}
/// 为视频文件生成缩略图
/// [videoPath] 要生成缩略图的视频文件路径
/// [thumbnailPath] 要生成的缩略图文件路径
/// 返回缩略图路径
static Future<String?> genVideoThumbnail(String videoPath, String thumbnailPath) async {
try {
// final thumbnailPath = '${dir.path}/video_thumb_${DateTime.now().millisecondsSinceEpoch}.jpg';
// 使用 ffmpeg_kit_flutter_new 生成视频缩略图
// 构建FFmpeg命令行参数
String cmd = '-i "$videoPath" ' // 指定输入文件路径
'-ss 1 ' // 从视频第1秒处截取画面
'-vframes 1 ' // 只截取一帧画面
'-vf scale=128:-1 ' // 设置缩略图宽度为128像素,高度按比例缩放
'-y ' // 覆盖已存在的输出文件
'"$thumbnailPath"'; // 指定输出文件路径
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
return thumbnailPath;
} else {
print('生成视频缩略图失败: ${await session.getFailStackTrace()}');
return null;
}
} catch (e) {
print('生成视频缩略图出错: $e');
return null;
}
}
}