24、批量提取:遍历文件夹,批量提取所有视频的音频轨道。
好,咱们终于来到了批量处理的环节。
说实话,前面几章我们一直在跟单个文件较劲。你可能会想:「我手头有几百个视频,难道要一个一个跑?」
嗯,当然不是。这一章,我们就来解决这个问题。
批量提取的核心思路
说白了,就是三件事:
- 遍历文件夹 —— 找到所有视频文件
- 逐个提取 —— 对每个文件调用 MediaExtractor
- 输出管理 —— 给每个音频文件起好名字,别搞混
我在项目中遇到过最尴尬的情况:批量跑完后,发现所有输出文件都叫「audio.aac」,后生成的把前面的全覆盖了。嗯,这种低级错误,犯过一次就够了。
核心原则:输入路径决定输出路径。保持目录结构,文件名自动生成。
遍历文件夹,筛选视频文件
Android 里遍历文件,我习惯用 File.listFiles() 配合递归。为什么不用 FileVisitor?因为 API 级别限制,有些低版本机型不支持。
你想想看,一个文件夹里可能有 .mp4、.mkv、.avi,也可能有 .txt、.jpg。我们只关心视频文件。
private List<File> findVideoFiles(File dir) {
List<File> videoList = new ArrayList<>();
File[] files = dir.listFiles();
if (files == null) return videoList;
for (File file : files) {
if (file.isDirectory()) {
// 递归遍历子目录
videoList.addAll(findVideoFiles(file));
} else {
String name = file.getName().toLowerCase();
if (name.endsWith(".mp4") || name.endsWith(".mkv")
|| name.endsWith(".avi") || name.endsWith(".mov")) {
videoList.add(file);
}
}
}
return videoList;
}
这段代码很直白。注意我用了 toLowerCase(),因为有些视频文件后缀是大写的,比如 .MP4。我曾经因为这个漏掉过文件,排查了半天才发现是大小写的问题。
批量提取的核心方法
有了文件列表,接下来就是循环调用提取逻辑。这里我封装了一个方法,输入一个视频文件,输出一个音频文件。
private void extractAudioFromVideo(File videoFile, File outputDir) {
String audioFileName = videoFile.getName().replaceFirst("[.][^.]+$", "") + ".aac";
File audioFile = new File(outputDir, audioFileName);
try (MediaExtractor extractor = new MediaExtractor();
FileOutputStream fos = new FileOutputStream(audioFile);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
extractor.setDataSource(videoFile.getAbsolutePath());
int audioTrackIndex = selectAudioTrack(extractor);
if (audioTrackIndex < 0) {
Log.w("BatchExtract", "No audio track in: " + videoFile.getName());
return;
}
extractor.selectTrack(audioTrackIndex);
MediaFormat format = extractor.getTrackFormat(audioTrackIndex);
String mime = format.getString(MediaFormat.KEY_MIME);
// 这里可以写一个简单的 AAC 封装,或者直接用 MediaMuxer
// 为了演示,我们只读取原始数据并写入文件(实际项目中建议用 MediaMuxer)
ByteBuffer buffer = ByteBuffer.allocate(500 * 1024);
int sampleSize;
while ((sampleSize = extractor.readSampleData(buffer, 0)) > 0) {
byte[] data = new byte[sampleSize];
buffer.position(0);
buffer.get(data, 0, sampleSize);
bos.write(data);
extractor.advance();
buffer.clear();
}
Log.i("BatchExtract", "Extracted: " + audioFile.getName());
} catch (IOException e) {
Log.e("BatchExtract", "Failed: " + videoFile.getName(), e);
}
}
小技巧:文件名用 replaceFirst 去掉原后缀,再拼接 .aac。这样「我的视频.mp4」会变成「我的视频.aac」,清晰明了。
主流程:遍历 + 提取
把上面两个方法组合起来,就是完整的批量提取流程。
public void batchExtract(String inputDirPath, String outputDirPath) {
File inputDir = new File(inputDirPath);
File outputDir = new File(outputDirPath);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
List<File> videoFiles = findVideoFiles(inputDir);
Log.i("BatchExtract", "Found " + videoFiles.size() + " video files.");
for (File videoFile : videoFiles) {
extractAudioFromVideo(videoFile, outputDir);
}
Log.i("BatchExtract", "Batch extraction completed.");
}
这里有个细节:outputDir.mkdirs() 一定要在遍历之前调用。我刚开始写的时候,把它放在了循环里,结果每个文件都要检查一次目录是否存在,纯属浪费性能。
流程图:批量提取的完整逻辑
下面这张图,把整个流程串起来了。你可以对照着看代码,会更清晰。
避坑指南
批量处理时,有几个坑我踩过,分享给你:
- 文件路径包含中文或空格 —— MediaExtractor 的
setDataSource接受路径字符串,但有些机型对中文路径支持不好。我建议用Uri.fromFile()的方式传入。 - 输出目录空间不足 —— 批量处理时,音频文件会占用大量空间。最好在开始前检查一下剩余存储空间。
- 异常中断 —— 如果某个视频文件损坏,提取会抛出异常。我习惯用 try-catch 包裹每个文件的提取逻辑,保证一个文件失败不影响其他文件。
- 线程阻塞 —— 批量提取是耗时操作,一定要放在子线程中执行,否则会卡住 UI。
注意:千万不要在主线程中调用批量提取。我曾经在测试时忘了切线程,结果应用直接 ANR,被测试同事追着骂了一下午。
性能优化建议
如果你要处理几百个视频,单线程逐个提取确实有点慢。我个人建议:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 单线程顺序提取 | 实现简单,资源占用低 | 速度慢,无法利用多核 |
| 线程池并行提取 | 速度快,充分利用 CPU | 需要控制并发数,避免 OOM |
| IntentService 后台提取 | 系统管理生命周期,适合后台任务 | 无法实时反馈进度 |
我一般用线程池,核心线程数设为 Runtime.getRuntime().availableProcessors(),这样既快又不会把手机搞卡。
小提示:如果你用线程池,记得给每个提取任务设置一个回调,用来更新进度条。用户看着进度条在动,心里会踏实很多。
总结一下
批量提取的核心,其实就是「遍历 + 循环」。但真正写起来,细节决定成败。
文件命名、异常处理、线程管理、空间检查……这些看似琐碎的东西,恰恰是工程质量的体现。我在项目里见过太多「能跑就行」的代码,结果上线后各种崩溃。
嗯,这一章的内容就到这里。代码不多,但思路很重要。你可以在自己的项目里试试,把批量提取封装成一个工具类,以后用起来就方便了。
公众号:蓝海资料掘金营,微信deep3321