1. 为什么选择MediaExtractor和MediaMuxer在Android开发中处理音视频时很多开发者第一反应是使用FFmpeg这样的第三方库。但你可能不知道Android系统本身就内置了两个强大的工具MediaExtractor和MediaMuxer。它们就像是Android系统自带的瑞士军刀专门用来处理音视频的解封装和封装操作。我刚开始接触音视频处理时也踩过不少坑。记得有一次为了给视频添加背景音乐折腾了整整两天FFmpeg的编译和集成。后来才发现原来用系统自带的API就能轻松实现这个功能。这两个类的最大优势就是不需要引入任何第三方库直接调用Android SDK就能使用既不会增加APK体积也不需要处理复杂的JNI调用。MediaExtractor主要负责从视频文件中拆包——把视频和音频数据分离出来。想象一下它就像是个专业的拆箱工人能把打包好的视频文件拆解成原始的视频帧和音频数据。而MediaMuxer则正好相反它是个打包专家能把处理好的音视频数据重新封装成完整的视频文件。2. 准备工作与环境搭建2.1 项目配置与权限设置在开始编码前我们需要做好基础准备工作。首先确保你的Android Studio项目已经配置了必要的依赖。虽然这两个类都是Android原生API但还是需要检查minSdkVersion是否满足要求android { compileSdkVersion 33 defaultConfig { minSdkVersion 21 targetSdkVersion 33 } }别忘了在AndroidManifest.xml中添加读写外部存储的权限uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE /对于Android 10及以上版本还需要在application标签中添加android:requestLegacyExternalStoragetrue2.2 理解关键概念在实际编码前有几个关键概念需要理解清楚轨道(Track)一个媒体文件可能包含多个轨道比如视频轨道、音频轨道甚至可能有字幕轨道。我们需要先确定要处理的是哪个轨道。时间戳(PresentationTimeUs)音视频同步的关键。视频的每一帧和音频的每一段数据都有自己的时间戳单位是微秒(μs)。BufferInfo这个类包含了写入数据时的重要信息包括数据大小、偏移量、时间戳和标志位。正确设置这些参数对最终生成的视频文件质量至关重要。3. 使用MediaExtractor提取音视频数据3.1 初始化与数据源设置让我们从MediaExtractor的基本使用开始。假设我们要处理一个存放在res/raw目录下的视频文件val extractor MediaExtractor() try { resources.openRawResourceFd(R.raw.sample_video).use { fd - extractor.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) } } catch (e: IOException) { e.printStackTrace() return }如果是处理SD卡中的文件可以这样设置数据源extractor.setDataSource(/sdcard/Movies/sample.mp4)注意在实际项目中请务必添加异常处理因为视频文件可能损坏或格式不支持。3.2 轨道选择与数据处理接下来我们需要遍历所有轨道找到我们需要的视频或音频轨道var videoTrackIndex -1 var audioTrackIndex -1 for (i in 0 until extractor.trackCount) { val format extractor.getTrackFormat(i) val mime format.getString(MediaFormat.KEY_MIME) when { mime?.startsWith(video/) true - { videoTrackIndex i extractor.selectTrack(i) } mime?.startsWith(audio/) true - { audioTrackIndex i extractor.selectTrack(i) } } }找到目标轨道后我们就可以开始读取数据了。这里有个小技巧根据轨道的最大输入大小来分配缓冲区val maxInputSize format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE) val buffer ByteBuffer.allocate(maxInputSize) val bufferInfo BufferInfo() while (true) { val sampleSize extractor.readSampleData(buffer, 0) if (sampleSize 0) break bufferInfo.size sampleSize bufferInfo.presentationTimeUs extractor.sampleTime bufferInfo.flags extractor.sampleFlags // 这里可以处理buffer中的数据 // ... extractor.advance() }4. 使用MediaMuxer封装新的视频文件4.1 创建Muxer与添加轨道MediaMuxer的使用相对简单但有几个关键点需要注意。首先创建Muxer实例val muxer MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)然后添加轨道。这里的轨道格式通常来自MediaExtractor提取的格式信息val videoFormat extractor.getTrackFormat(videoTrackIndex) val audioFormat extractor.getTrackFormat(audioTrackIndex) val muxerVideoTrackIndex muxer.addTrack(videoFormat) val muxerAudioTrackIndex muxer.addTrack(audioFormat)4.2 数据写入与同步添加完所有轨道后就可以开始写入数据了。但有个重要细节必须在添加完所有轨道后才能调用start()方法muxer.start() // 写入视频数据 extractor.selectTrack(videoTrackIndex) while (/* 读取视频数据 */) { muxer.writeSampleData(muxerVideoTrackIndex, buffer, bufferInfo) } // 写入音频数据 extractor.selectTrack(audioTrackIndex) while (/* 读取音频数据 */) { muxer.writeSampleData(muxerAudioTrackIndex, buffer, bufferInfo) }提示在实际项目中你可能需要处理音视频同步问题。一个常见的做法是以视频时间轴为基准调整音频时间戳。5. 实战视频剪辑与音频替换5.1 剪辑视频片段现在我们来解决一个实际需求从长视频中剪辑出30秒的精彩片段。关键点在于控制读取数据的时间范围val startTimeUs 10_000_000 // 从第10秒开始 val endTimeUs 40_000_000 // 到第40秒结束 extractor.selectTrack(videoTrackIndex) while (true) { val sampleTime extractor.sampleTime if (sampleTime startTimeUs) { extractor.advance() continue } if (sampleTime endTimeUs) break // 读取并写入数据 // ... }5.2 替换背景音乐另一个常见需求是替换视频的背景音乐。我们需要两个MediaExtractor实例一个处理原视频一个处理新音频val videoExtractor MediaExtractor() val audioExtractor MediaExtractor() // 设置数据源 videoExtractor.setDataSource(videoPath) audioExtractor.setDataSource(audioPath) // 选择轨道 videoExtractor.selectTrack(videoTrackIndex) audioExtractor.selectTrack(audioTrackIndex) // 创建Muxer并添加轨道 val muxer MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) val muxerVideoTrackIndex muxer.addTrack(videoExtractor.getTrackFormat(videoTrackIndex)) val muxerAudioTrackIndex muxer.addTrack(audioExtractor.getTrackFormat(audioTrackIndex)) muxer.start() // 写入视频数据 while (/* 读取视频数据 */) { muxer.writeSampleData(muxerVideoTrackIndex, videoBuffer, videoBufferInfo) } // 写入音频数据 while (/* 读取音频数据 */) { // 可以在这里调整音频时间戳使其与视频同步 audioBufferInfo.presentationTimeUs videoDurationUs * position / totalFrames muxer.writeSampleData(muxerAudioTrackIndex, audioBuffer, audioBufferInfo) }6. 性能优化与常见问题6.1 内存管理与大文件处理处理大视频文件时内存管理尤为重要。我曾在项目中遇到过OOM(内存溢出)问题后来通过以下方法解决使用合适的缓冲区大小不要一次性分配过大缓冲区可以根据实际需要动态调整。及时释放资源处理完成后立即调用release()方法。分块处理对于超大文件可以考虑分段处理后再合并。// 优化后的缓冲区分配 val format extractor.getTrackFormat(trackIndex) val maxInputSize format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE) val buffer ByteBuffer.allocateDirect(maxInputSize) // 使用直接缓冲区6.2 常见错误与解决方案在实际开发中你可能会遇到这些问题无法播放生成的视频检查是否正确地调用了muxer.stop()缺少这一步会导致文件不完整。音视频不同步确保写入数据时的时间戳是正确的特别是当剪辑视频中间片段时。文件损坏检查BufferInfo中的size参数是否正确设置了实际数据大小。添加轨道失败确保在调用muxer.start()前添加了所有轨道。7. 扩展应用更多实用场景除了基本的剪辑和音频替换这两个API还能实现更多有趣的功能视频拼接通过多个MediaExtractor读取不同视频然后按顺序写入同一个MediaMuxer。提取音频只选择音频轨道进行处理可以制作手机铃声或背景音乐。视频静音只写入视频轨道数据不添加音频轨道。格式转换虽然不能直接转码但可以改变封装格式如从MP4改为WebM。// 视频静音示例 val extractor MediaExtractor() extractor.setDataSource(videoPath) extractor.selectTrack(videoTrackIndex) val muxer MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) muxer.addTrack(extractor.getTrackFormat(videoTrackIndex)) muxer.start() // 只写入视频数据 while (/* 读取视频数据 */) { muxer.writeSampleData(0, buffer, bufferInfo) }8. 完整示例代码为了帮助你更好地理解这里提供一个完整的视频剪辑示例包含异常处理和资源释放fun clipVideo(inputPath: String, outputPath: String, startSec: Int, endSec: Int): Boolean { val extractor MediaExtractor() val muxer MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) try { // 设置输入文件 extractor.setDataSource(inputPath) // 查找视频轨道 var videoTrackIndex -1 for (i in 0 until extractor.trackCount) { val format extractor.getTrackFormat(i) if (format.getString(MediaFormat.KEY_MIME)?.startsWith(video/) true) { videoTrackIndex i break } } if (videoTrackIndex -1) { Log.e(VideoClipper, No video track found) return false } // 配置Muxer extractor.selectTrack(videoTrackIndex) muxer.addTrack(extractor.getTrackFormat(videoTrackIndex)) muxer.start() // 时间转换(秒转微秒) val startUs startSec * 1_000_000L val endUs endSec * 1_000_000L // 定位到起始位置 extractor.seekTo(startUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC) // 准备缓冲区 val format extractor.getTrackFormat(videoTrackIndex) val buffer ByteBuffer.allocate(format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)) val bufferInfo BufferInfo() // 处理视频数据 while (true) { val sampleTime extractor.sampleTime if (sampleTime startUs) { extractor.advance() continue } if (sampleTime endUs) break bufferInfo.size extractor.readSampleData(buffer, 0) if (bufferInfo.size 0) break bufferInfo.presentationTimeUs sampleTime - startUs // 调整时间戳 bufferInfo.flags extractor.sampleFlags muxer.writeSampleData(0, buffer, bufferInfo) extractor.advance() } return true } catch (e: Exception) { Log.e(VideoClipper, Error clipping video, e) return false } finally { extractor.release() try { muxer.stop() muxer.release() } catch (e: Exception) { Log.e(VideoClipper, Error releasing muxer, e) } } }这个示例展示了如何安全地处理视频剪辑的完整流程包括异常处理和资源释放。在实际项目中你可以根据需要扩展这个基础功能比如添加音频处理或更复杂的时间轴控制。记得在实际使用时要在后台线程执行这些操作避免阻塞UI线程。处理完成后可以通过FileProvider来访问生成的文件或者直接显示在应用的UI中。