24、批量提取:遍历文件夹,批量提取所有视频的音频轨道。

好,咱们终于来到了批量处理的环节。

说实话,前面几章我们一直在跟单个文件较劲。你可能会想:「我手头有几百个视频,难道要一个一个跑?」

嗯,当然不是。这一章,我们就来解决这个问题。

批量提取的核心思路

说白了,就是三件事:

  1. 遍历文件夹 —— 找到所有视频文件
  2. 逐个提取 —— 对每个文件调用 MediaExtractor
  3. 输出管理 —— 给每个音频文件起好名字,别搞混

我在项目中遇到过最尴尬的情况:批量跑完后,发现所有输出文件都叫「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() 一定要在遍历之前调用。我刚开始写的时候,把它放在了循环里,结果每个文件都要检查一次目录是否存在,纯属浪费性能。

流程图:批量提取的完整逻辑

下面这张图,把整个流程串起来了。你可以对照着看代码,会更清晰。

批量提取音频流程图 开始:输入文件夹路径 递归遍历文件夹,筛选视频文件 是否有视频文件? 循环:逐个提取音频 输出:生成 .aac 文件 循环 结束:无文件 结束:批量提取完成

避坑指南

批量处理时,有几个坑我踩过,分享给你:

  • 文件路径包含中文或空格 —— MediaExtractor 的 setDataSource 接受路径字符串,但有些机型对中文路径支持不好。我建议用 Uri.fromFile() 的方式传入。
  • 输出目录空间不足 —— 批量处理时,音频文件会占用大量空间。最好在开始前检查一下剩余存储空间。
  • 异常中断 —— 如果某个视频文件损坏,提取会抛出异常。我习惯用 try-catch 包裹每个文件的提取逻辑,保证一个文件失败不影响其他文件。
  • 线程阻塞 —— 批量提取是耗时操作,一定要放在子线程中执行,否则会卡住 UI。

注意:千万不要在主线程中调用批量提取。我曾经在测试时忘了切线程,结果应用直接 ANR,被测试同事追着骂了一下午。

性能优化建议

如果你要处理几百个视频,单线程逐个提取确实有点慢。我个人建议:

方案 优点 缺点
单线程顺序提取 实现简单,资源占用低 速度慢,无法利用多核
线程池并行提取 速度快,充分利用 CPU 需要控制并发数,避免 OOM
IntentService 后台提取 系统管理生命周期,适合后台任务 无法实时反馈进度

我一般用线程池,核心线程数设为 Runtime.getRuntime().availableProcessors(),这样既快又不会把手机搞卡。

小提示:如果你用线程池,记得给每个提取任务设置一个回调,用来更新进度条。用户看着进度条在动,心里会踏实很多。

总结一下

批量提取的核心,其实就是「遍历 + 循环」。但真正写起来,细节决定成败。

文件命名、异常处理、线程管理、空间检查……这些看似琐碎的东西,恰恰是工程质量的体现。我在项目里见过太多「能跑就行」的代码,结果上线后各种崩溃。

嗯,这一章的内容就到这里。代码不多,但思路很重要。你可以在自己的项目里试试,把批量提取封装成一个工具类,以后用起来就方便了。


公众号:蓝海资料掘金营,微信deep3321