Android MediaCodec:深入理解多媒体编解码核心
本文全面解析了 Android MediaCodec 多媒体编解码框架的核心原理与实践应用。文章详细阐述了 MediaCodec 的异步工作模式、编解码流程及性能优化策略,并重点扩展了多媒体文件解析能力。通过 MediaExtractor,开发者可精准获取 MP3/MP4 等文件的采样率、声道数、比特率、时长与轨道信息。文章提供了从 MP3 文件中解码提取原始 PCM 数据的完整示例代码,涵盖文件分析、解码器配置、实时数据回调及 PCM 波形处理等关键环节。此外,还总结了缓冲区管理、低延迟处理及资源释放的最佳实践,为开发高效稳定的音视频处理应用提供了全面指导。
博主博客
目录
概述
Android MediaCodec 是 Android 多媒体框架中的核心编解码组件,自 Android 4.1(API 16)引入,提供了对音频和视频进行硬件加速编解码的能力。
主要特性
- ✅ 硬件加速:充分利用设备硬件编解码器
- ✅ 低延迟:适用于实时音视频处理
- ✅ 多种格式支持:H.264, H.265, VP8, VP9, AAC, MP3 等
- ✅ 灵活的数据处理:支持 ByteBuffer 和 Surface 输入
核心架构
// MediaCodec 的基本组成
MediaCodec ────┬─── Codec (编码器/解码器)
├─── Input Buffer (输入缓冲区)
├─── Output Buffer (输出缓冲区)
└─── Format (媒体格式配置)
缓冲区管理
MediaCodec 使用异步缓冲区管理机制:
- 输入缓冲区:存放待处理的原始数据
- 输出缓冲区:存放处理后的结果数据
工作模式
1. 同步模式(已废弃)
// API 21 之前的使用方式(不推荐)
dequeueInputBuffer(timeout) → 填充数据 → queueInputBuffer()
dequeueOutputBuffer(timeout) → 处理数据 → releaseOutputBuffer()
2. 异步模式(推荐)
// API 21+ 推荐使用方式
mediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 处理输入缓冲区
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
MediaCodec.BufferInfo info) {
// 处理输出缓冲区
}
});
媒体文件信息读取
使用 MediaExtractor 读取 MP3/MP4 文件信息
class MediaFileAnalyzer {
/**
* 获取媒体文件详细信息
* @param filePath 媒体文件路径
* @return 包含所有媒体信息的 Map
*/
fun analyzeMediaFile(filePath: String): Map<String, Any> {
val extractor = MediaExtractor()
val result = mutableMapOf<String, Any>()
try {
// 设置数据源
extractor.setDataSource(filePath)
// 获取轨道数量
val trackCount = extractor.trackCount
result["trackCount"] = trackCount
val trackInfoList = mutableListOf<Map<String, Any>>()
// 遍历所有轨道
for (i in 0 until trackCount) {
val trackInfo = analyzeTrack(extractor, i)
trackInfoList.add(trackInfo)
}
result["tracks"] = trackInfoList
// 获取总时长(单位:微秒)
val durationUs = extractor.getTrackFormat(0)
.getLong(MediaFormat.KEY_DURATION)
result["duration"] = durationUs / 1000 // 转换为毫秒
} catch (e: Exception) {
Log.e("MediaFileAnalyzer", "分析文件失败: ${e.message}")
} finally {
extractor.release()
}
return result
}
private fun analyzeTrack(extractor: MediaExtractor, trackIndex: Int): Map<String, Any> {
val format = extractor.getTrackFormat(trackIndex)
val trackInfo = mutableMapOf<String, Any>()
// 获取 MIME 类型
val mime = format.getString(MediaFormat.KEY_MIME) ?: "unknown"
trackInfo["mimeType"] = mime
trackInfo["trackIndex"] = trackIndex
// 根据 MIME 类型解析不同信息
when {
mime.startsWith("audio/") -> {
// 音频轨道信息
trackInfo["type"] = "audio"
trackInfo["sampleRate"] = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
trackInfo["channelCount"] = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
// 获取比特率(如果存在)
if (format.containsKey(MediaFormat.KEY_BIT_RATE)) {
trackInfo["bitrate"] = format.getInteger(MediaFormat.KEY_BIT_RATE)
}
// 获取编码配置(AAC 特定)
if (format.containsKey(MediaFormat.KEY_AAC_PROFILE)) {
trackInfo["aacProfile"] = format.getInteger(MediaFormat.KEY_AAC_PROFILE)
}
}
mime.startsWith("video/") -> {
// 视频轨道信息
trackInfo["type"] = "video"
trackInfo["width"] = format.getInteger(MediaFormat.KEY_WIDTH)
trackInfo["height"] = format.getInteger(MediaFormat.KEY_HEIGHT)
// 获取帧率
if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
trackInfo["frameRate"] = format.getInteger(MediaFormat.KEY_FRAME_RATE)
}
// 获取颜色格式
if (format.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
trackInfo["colorFormat"] = format.getInteger(MediaFormat.KEY_COLOR_FORMAT)
}
}
}
// 获取时长
if (format.containsKey(MediaFormat.KEY_DURATION)) {
trackInfo["duration"] = format.getLong(MediaFormat.KEY_DURATION) / 1000
}
// 获取最大输入大小
if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
trackInfo["maxInputSize"] = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
}
return trackInfo
}
/**
* 打印媒体文件详细信息
*/
fun printMediaInfo(filePath: String) {
val info = analyzeMediaFile(filePath)
println("=== 媒体文件分析结果 ===")
println("文件路径: $filePath")
println("总时长: ${info["duration"]} ms")
println("轨道数量: ${info["trackCount"]}")
val tracks = info["tracks"] as List<Map<String, Any>>
tracks.forEach { track ->
println("\n--- 轨道 ${track["trackIndex"]} ---")
println("类型: ${track["type"]}")
println("MIME: ${track["mimeType"]}")
when (track["type"]) {
"audio" -> {
println("采样率: ${track["sampleRate"]} Hz")
println("声道数: ${track["channelCount"]}")
track["bitrate"]?.let { println("比特率: $it bps") }
}
"video" -> {
println("分辨率: ${track["width"]}x${track["height"]}")
track["frameRate"]?.let { println("帧率: $it fps") }
}
}
track["duration"]?.let { println("轨道时长: $it ms") }
}
}
}
使用示例
// 读取 MP3/MP4 文件信息
val analyzer = MediaFileAnalyzer()
// 分析 MP3 文件
val mp3Info = analyzer.analyzeMediaFile("/sdcard/music/sample.mp3")
analyzer.printMediaInfo("/sdcard/music/sample.mp3")
// 分析 MP4 文件
val mp4Info = analyzer.analyzeMediaFile("/sdcard/videos/sample.mp4")
analyzer.printMediaInfo("/sdcard/videos/sample.mp4")
基本使用流程
MP3 解码获取 PCM 数据流程
graph TD
A[创建 MediaExtractor] --> B[设置数据源]
B --> C[选择音频轨道]
C --> D[获取音频格式]
D --> E[创建 MediaCodec 解码器]
E --> F[配置解码器]
F --> G[启动解码器]
G --> H{循环处理}
H --> I[读取编码数据]
I --> J[解码为 PCM]
J --> K[处理 PCM 数据]
K --> H
K --> L[到达文件末尾]
L --> M[停止并释放资源]
代码示例
示例1:从 MP3 文件提取 PCM 数据
class MP3ToPCMExtractor {
companion object {
private const val TIMEOUT_US = 10000L
private const val BUFFER_SIZE = 64 * 1024 // 64KB 缓冲区
}
/**
* 提取 MP3 文件的 PCM 数据
* @param inputPath MP3 文件路径
* @param outputPath PCM 输出文件路径(可选)
* @param onPCMData 回调处理 PCM 数据
*/
fun extractPCMFromMP3(
inputPath: String,
outputPath: String? = null,
onPCMData: ((ByteArray, MediaCodec.BufferInfo) -> Unit)? = null
) {
var extractor: MediaExtractor? = null
var decoder: MediaCodec? = null
var outputFile: FileOutputStream? = null
try {
// 1. 创建 MediaExtractor 并设置数据源
extractor = MediaExtractor()
extractor.setDataSource(inputPath)
// 2. 选择音频轨道
val audioTrackIndex = selectAudioTrack(extractor)
if (audioTrackIndex == -1) {
throw IllegalStateException("未找到音频轨道")
}
extractor.selectTrack(audioTrackIndex)
val format = extractor.getTrackFormat(audioTrackIndex)
// 3. 打印音频信息
printAudioInfo(format)
// 4. 创建解码器
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
decoder = MediaCodec.createDecoderByType(mime)
// 5. 配置解码器(不指定 Surface,用于获取 PCM)
decoder.configure(format, null, null, 0)
// 6. 启动解码器
decoder.start()
// 7. 如果需要,创建输出文件
if (!outputPath.isNullOrEmpty()) {
outputFile = FileOutputStream(outputPath)
}
// 8. 开始解码循环
decodeLoop(extractor, decoder, outputFile, onPCMData)
} catch (e: Exception) {
Log.e("MP3ToPCMExtractor", "提取 PCM 失败: ${e.message}")
e.printStackTrace()
} finally {
// 9. 清理资源
decoder?.stop()
decoder?.release()
extractor?.release()
outputFile?.close()
}
}
/**
* 选择音频轨道
*/
private fun selectAudioTrack(extractor: MediaExtractor): Int {
for (i in 0 until extractor.trackCount) {
val format = extractor.getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
if (mime.startsWith("audio/")) {
return i
}
}
return -1
}
/**
* 打印音频信息
*/
private fun printAudioInfo(format: MediaFormat) {
println("=== 音频信息 ===")
println("MIME 类型: ${format.getString(MediaFormat.KEY_MIME)}")
println("采样率: ${format.getInteger(MediaFormat.KEY_SAMPLE_RATE)} Hz")
println("声道数: ${format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)}")
if (format.containsKey(MediaFormat.KEY_DURATION)) {
val durationMs = format.getLong(MediaFormat.KEY_DURATION) / 1000
println("时长: ${durationMs} ms")
}
if (format.containsKey(MediaFormat.KEY_BIT_RATE)) {
println("比特率: ${format.getInteger(MediaFormat.KEY_BIT_RATE)} bps")
}
if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
println("最大输入大小: ${format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)} bytes")
}
}
/**
* 解码循环
*/
private fun decodeLoop(
extractor: MediaExtractor,
decoder: MediaCodec,
outputFile: FileOutputStream?,
onPCMData: ((ByteArray, MediaCodec.BufferInfo) -> Unit)?
) {
val bufferInfo = MediaCodec.BufferInfo()
var inputEOS = false
var outputEOS = false
while (!outputEOS) {
// 处理输入
if (!inputEOS) {
val inputBufferIndex = decoder.dequeueInputBuffer(TIMEOUT_US)
if (inputBufferIndex >= 0) {
val inputBuffer = decoder.getInputBuffer(inputBufferIndex)
val sampleSize = extractor.readSampleData(inputBuffer!!, 0)
if (sampleSize < 0) {
// 文件结束
decoder.queueInputBuffer(
inputBufferIndex,
0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
inputEOS = true
} else {
// 读取数据
val presentationTimeUs = extractor.sampleTime
decoder.queueInputBuffer(
inputBufferIndex,
0, sampleSize,
presentationTimeUs, 0
)
extractor.advance()
}
}
}
// 处理输出
val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
when (outputBufferIndex) {
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
Log.d("MP3ToPCMExtractor", "输出缓冲区改变")
}
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
val newFormat = decoder.outputFormat
Log.d("MP3ToPCMExtractor", "输出格式改变: $newFormat")
}
MediaCodec.INFO_TRY_AGAIN_LATER -> {
// 没有可用的输出缓冲区
}
else -> {
if (outputBufferIndex >= 0) {
// 获取 PCM 数据
val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)
outputBuffer?.let { buffer ->
// 确保 bufferInfo.size 有效
if (bufferInfo.size > 0) {
// 创建 PCM 数据数组
val pcmData = ByteArray(bufferInfo.size)
buffer.position(bufferInfo.offset)
buffer.limit(bufferInfo.offset + bufferInfo.size)
buffer.get(pcmData, 0, bufferInfo.size)
// 回调处理 PCM 数据
onPCMData?.invoke(pcmData, bufferInfo)
// 写入文件(如果需要)
outputFile?.write(pcmData)
// 输出调试信息(可选)
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
Log.v("MP3ToPCMExtractor",
"解码帧: size=${bufferInfo.size}, " +
"time=${bufferInfo.presentationTimeUs}us")
}
}
}
// 释放输出缓冲区
decoder.releaseOutputBuffer(outputBufferIndex, false)
// 检查是否结束
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
outputEOS = true
Log.d("MP3ToPCMExtractor", "解码完成")
}
}
}
}
}
}
/**
* 实时处理 PCM 数据的回调版本
*/
fun extractPCMWithCallback(
inputPath: String,
onAudioInfo: (MediaFormat) -> Unit,
onPCMDataAvailable: (ByteArray, Long) -> Unit,
onCompletion: () -> Unit
) {
// 使用异步回调模式
val extractor = MediaExtractor()
try {
extractor.setDataSource(inputPath)
val audioTrackIndex = selectAudioTrack(extractor)
extractor.selectTrack(audioTrackIndex)
val format = extractor.getTrackFormat(audioTrackIndex)
onAudioInfo(format)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
val decoder = MediaCodec.createDecoderByType(mime)
decoder.setCallback(object : MediaCodec.Callback() {
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
val inputBuffer = codec.getInputBuffer(index)
val sampleSize = extractor.readSampleData(inputBuffer!!, 0)
if (sampleSize >= 0) {
val presentationTimeUs = extractor.sampleTime
codec.queueInputBuffer(
index, 0, sampleSize,
presentationTimeUs, 0
)
extractor.advance()
} else {
codec.queueInputBuffer(
index, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
}
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
if (info.size > 0) {
val outputBuffer = codec.getOutputBuffer(index)
outputBuffer?.let { buffer ->
val pcmData = ByteArray(info.size)
buffer.position(info.offset)
buffer.limit(info.offset + info.size)
buffer.get(pcmData, 0, info.size)
// 回调 PCM 数据
onPCMDataAvailable(pcmData, info.presentationTimeUs)
}
}
codec.releaseOutputBuffer(index, false)
if ((info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
extractor.release()
decoder.stop()
decoder.release()
onCompletion()
}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
Log.e("MP3ToPCMExtractor", "解码错误: ${e.message}")
}
override fun onOutputFormatChanged(
codec: MediaCodec,
format: MediaFormat
) {
Log.d("MP3ToPCMExtractor", "输出格式改变: $format")
}
})
decoder.configure(format, null, null, 0)
decoder.start()
} catch (e: Exception) {
Log.e("MP3ToPCMExtractor", "处理失败: ${e.message}")
extractor.release()
}
}
}
示例2:使用示例和数据处理
class AudioProcessor {
/**
* 示例:处理 MP3 文件的完整流程
*/
fun processMP3File() {
val inputPath = "/sdcard/music/test.mp3"
val outputPath = "/sdcard/music/output.pcm"
val extractor = MP3ToPCMExtractor()
// 方式1:提取 PCM 到文件
extractor.extractPCMFromMP3(inputPath, outputPath) { pcmData, bufferInfo ->
// 实时处理 PCM 数据
val sampleRate = 44100 // 从 MediaFormat 获取的实际值
val channelCount = 2 // 从 MediaFormat 获取的实际值
processPCMData(pcmData, sampleRate, channelCount, bufferInfo.presentationTimeUs)
}
// 方式2:使用回调方式处理
extractor.extractPCMWithCallback(
inputPath = inputPath,
onAudioInfo = { format ->
val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
val channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
val duration = format.getLong(MediaFormat.KEY_DURATION) / 1000
println("音频信息:")
println(" 采样率: $sampleRate Hz")
println(" 声道数: $channelCount")
println(" 时长: $duration ms")
},
onPCMDataAvailable = { pcmData, timestamp ->
// 实时处理 PCM 数据
visualizeAudioWaveform(pcmData)
calculateVolumeLevel(pcmData)
},
onCompletion = {
println("PCM 数据提取完成")
}
)
}
/**
* 处理 PCM 数据的实用方法
*/
private fun processPCMData(
pcmData: ByteArray,
sampleRate: Int,
channelCount: Int,
timestampUs: Long
) {
// 1. 将字节数组转换为短整型数组(16位 PCM)
val samples = ShortArray(pcmData.size / 2)
ByteBuffer.wrap(pcmData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples)
// 2. 分离声道(如果是立体声)
if (channelCount == 2) {
val leftChannel = mutableListOf<Short>()
val rightChannel = mutableListOf<Short>()
for (i in samples.indices step 2) {
leftChannel.add(samples[i])
rightChannel.add(samples[i + 1])
}
// 处理左右声道数据...
}
// 3. 计算音频特征
val volume = calculateRMS(samples)
val frequency = estimateFrequency(samples, sampleRate)
// 4. 记录或显示信息
Log.d("AudioProcessor",
"时间: ${timestampUs / 1000}ms, " +
"音量: $volume, " +
"估计频率: $frequency Hz")
}
/**
* 计算 RMS(均方根)音量
*/
private fun calculateRMS(samples: ShortArray): Double {
var sum = 0.0
for (sample in samples) {
val value = sample.toDouble() / Short.MAX_VALUE
sum += value * value
}
return sqrt(sum / samples.size)
}
/**
* 估计频率(零交叉法,简单实现)
*/
private fun estimateFrequency(samples: ShortArray, sampleRate: Int): Double {
var zeroCrossings = 0
for (i in 1 until samples.size) {
if (samples[i] >= 0 && samples[i - 1] < 0 ||
samples[i] < 0 && samples[i - 1] >= 0) {
zeroCrossings++
}
}
val duration = samples.size.toDouble() / sampleRate
return zeroCrossings / (2.0 * duration)
}
/**
* 可视化音频波形
*/
private fun visualizeAudioWaveform(pcmData: ByteArray) {
// 简单的波形可视化(控制台输出)
val samples = ShortArray(pcmData.size / 2)
ByteBuffer.wrap(pcmData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples)
// 取前 50 个采样点显示
val displayLength = min(50, samples.size)
val waveform = StringBuilder()
for (i in 0 until displayLength) {
val normalized = (samples[i].toDouble() / Short.MAX_VALUE * 20).toInt()
waveform.append(" ".repeat(20 + normalized)).append("*\n")
}
// 清除控制台并显示波形(实际应用中应该用 UI 组件)
// println("\u001B[2J\u001B[H") // 清屏
// println(waveform.toString())
}
/**
* 计算音量级别
*/
private fun calculateVolumeLevel(pcmData: ByteArray): Float {
val samples = ShortArray(pcmData.size / 2)
ByteBuffer.wrap(pcmData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples)
var sum = 0L
for (sample in samples) {
sum += Math.abs(sample.toLong())
}
val average = sum.toFloat() / samples.size
val normalized = average / Short.MAX_VALUE
// 转换为分贝(简化计算)
val db = 20 * log10(normalized.toDouble())
return db.toFloat()
}
}
// 使用示例
fun main() {
val processor = AudioProcessor()
processor.processMP3File()
// 读取媒体文件信息
val analyzer = MediaFileAnalyzer()
analyzer.printMediaInfo("/sdcard/music/sample.mp3")
// 输出示例:
// === 媒体文件分析结果 ===
// 文件路径: /sdcard/music/sample.mp3
// 总时长: 240000 ms
// 轨道数量: 1
//
// --- 轨道 0 ---
// 类型: audio
// MIME: audio/mpeg
// 采样率: 44100 Hz
// 声道数: 2
// 比特率: 128000 bps
// 轨道时长: 240000 ms
}
示例3:获取媒体文件元数据
class MediaMetadataExtractor {
/**
* 获取媒体文件的完整元数据
*/
fun extractMetadata(filePath: String): MediaMetadata {
val metadata = MediaMetadata()
val extractor = MediaExtractor()
try {
extractor.setDataSource(filePath)
// 获取轨道信息
for (i in 0 until extractor.trackCount) {
val format = extractor.getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
when {
mime.startsWith("audio/") -> {
metadata.audioTrack = AudioTrackInfo(
sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE),
channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
bitrate = if (format.containsKey(MediaFormat.KEY_BIT_RATE)) {
format.getInteger(MediaFormat.KEY_BIT_RATE)
} else null,
durationUs = format.getLong(MediaFormat.KEY_DURATION),
mimeType = mime
)
}
mime.startsWith("video/") -> {
metadata.videoTrack = VideoTrackInfo(
width = format.getInteger(MediaFormat.KEY_WIDTH),
height = format.getInteger(MediaFormat.KEY_HEIGHT),
frameRate = if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
format.getInteger(MediaFormat.KEY_FRAME_RATE)
} else null,
bitrate = if (format.containsKey(MediaFormat.KEY_BIT_RATE)) {
format.getInteger(MediaFormat.KEY_BIT_RATE)
} else null,
durationUs = format.getLong(MediaFormat.KEY_DURATION),
mimeType = mime
)
}
}
}
} catch (e: Exception) {
Log.e("MediaMetadataExtractor", "提取元数据失败", e)
} finally {
extractor.release()
}
return metadata
}
data class MediaMetadata(
var audioTrack: AudioTrackInfo? = null,
var videoTrack: VideoTrackInfo? = null,
var fileSize: Long = 0,
var filePath: String = ""
)
data class AudioTrackInfo(
val sampleRate: Int,
val channelCount: Int,
val bitrate: Int?,
val durationUs: Long,
val mimeType: String
) {
val durationMs: Long get() = durationUs / 1000
val isStereo: Boolean get() = channelCount >= 2
}
data class VideoTrackInfo(
val width: Int,
val height: Int,
val frameRate: Int?,
val bitrate: Int?,
val durationUs: Long,
val mimeType: String
) {
val durationMs: Long get() = durationUs / 1000
val resolution: String get() = "${width}x${height}"
val aspectRatio: Float get() = width.toFloat() / height
}
}
高级特性
1. Surface 输入/输出
// 使用 Surface 作为输入(相机预览)
val surface = mediaCodec.createInputSurface()
// 使用 Surface 作为输出(渲染到视图)
mediaCodec.configure(format, surface, null, 0)
2. 动态配置
// 动态调整比特率
val params = Bundle().apply {
putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, newBitrate)
}
mediaCodec.setParameters(params)
3. 低延迟模式
// 配置低延迟编码
format.setInteger(MediaFormat.KEY_LATENCY, 1)
最佳实践
✅ 建议做法
-
总是检查设备能力
fun isCodecSupported(mimeType: String): Boolean { val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) return codecList.findDecoderForFormat(format) != null } -
正确处理生命周期
override fun onPause() { mediaCodec.stop() } override fun onResume() { mediaCodec.start() } override fun onDestroy() { mediaCodec.release() } -
使用合适的缓冲区大小
// 根据 PCM 参数计算缓冲区大小 fun calculatePCMBufferSize(sampleRate: Int, channelCount: Int, durationMs: Int): Int { val bytesPerSample = 2 // 16-bit PCM return sampleRate * channelCount * bytesPerSample * durationMs / 1000 }
PCM 数据处理的最佳实践
-
缓冲管理
class PCMSampleBuffer { private val buffer = ByteArrayOutputStream() private val lock = Object() fun write(data: ByteArray) { synchronized(lock) { buffer.write(data) } } fun read(size: Int): ByteArray? { synchronized(lock) { return if (buffer.size() >= size) { val result = buffer.toByteArray().copyOfRange(0, size) val remaining = ByteArray(buffer.size() - size) System.arraycopy(buffer.toByteArray(), size, remaining, 0, remaining.size) buffer.reset() buffer.write(remaining) result } else { null } } } } -
实时处理优化
class RealTimeAudioProcessor { private val executor = Executors.newSingleThreadExecutor() fun processInBackground(pcmData: ByteArray, callback: (ByteArray) -> Unit) { executor.submit { // 在后台线程处理音频数据 val processed = applyEffects(pcmData) callback(processed) } } private fun applyEffects(data: ByteArray): ByteArray { // 应用均衡器、混响等效果 return data // 简化实现 } }
常见问题
Q1: 编解码器初始化失败
可能原因:
- 不支持的格式
- 资源不足
- 错误的参数配置
解决方案:
try {
mediaCodec = MediaCodec.createDecoderByType(mimeType)
} catch (e: IOException) {
// 尝试软件解码器
mediaCodec = MediaCodec.createByCodecName("OMX.google.h264.decoder")
}
Q2: PCM 数据获取问题
常见问题:
-
采样率不正确
// 验证采样率 val format = extractor.getTrackFormat(audioTrackIndex) val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) require(sampleRate in listOf(8000, 16000, 44100, 48000)) { "不支持的采样率: $sampleRate" } -
声道数错误
val channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) when (channelCount) { 1 -> println("单声道") 2 -> println("立体声") else -> println("多声道: $channelCount") }
Q3: 内存泄漏
预防措施:
class SafeMediaCodecWrapper {
private val mediaCodec: MediaCodec
init {
mediaCodec = // 初始化
}
fun release() {
try {
mediaCodec.stop()
} catch (e: Exception) {
// 记录日志但不抛出
}
try {
mediaCodec.release()
} catch (e: Exception) {
// 记录日志
}
}
}
性能优化建议
1. PCM 数据处理的优化
class OptimizedPCMProcessor {
// 使用直接缓冲区减少拷贝
private val directBuffer: ByteBuffer = ByteBuffer.allocateDirect(1024 * 1024)
fun processPCMEfficiently(pcmData: ByteArray): ByteArray {
// 1. 批量处理,减少函数调用开销
val batchSize = 1024
val result = ByteArrayOutputStream()
for (i in pcmData.indices step batchSize) {
val end = min(i + batchSize, pcmData.size)
val batch = pcmData.copyOfRange(i, end)
// 2. 使用直接缓冲区
directBuffer.clear()
directBuffer.put(batch)
directBuffer.flip()
// 3. 处理数据
processBatch(directBuffer)
// 4. 收集结果
val processed = ByteArray(directBuffer.remaining())
directBuffer.get(processed)
result.write(processed)
}
return result.toByteArray()
}
private fun processBatch(buffer: ByteBuffer) {
// 高效批处理逻辑
}
}
2. 内存优化
// 重用缓冲区
object PCMBufferPool {
private val pool = mutableListOf<ByteArray>()
fun getBuffer(size: Int): ByteArray {
return pool.find { it.size == size } ?: ByteArray(size).also {
pool.add(it)
}
}
fun returnBuffer(buffer: ByteArray) {
// 可以清理缓冲区内容
buffer.fill(0)
}
}
总结
本文详细介绍了 Android MediaCodec 的核心功能,特别增加了:
关键增强内容:
-
媒体文件信息读取:
- 使用 MediaExtractor 获取 MP3/MP4 的采样率、声道数、时长
- 支持音频和视频轨道的全面信息提取
-
PCM 数据提取:
- 完整的 MP3 解码到 PCM 的示例
- 实时 PCM 数据处理和回调机制
- PCM 数据的实用处理方法(音量计算、波形可视化等)
-
实践指导:
- 如何正确配置解码器获取原始 PCM
- PCM 数据的缓冲管理和处理优化
- 常见问题的解决方案
核心要点:
- MediaExtractor 是读取媒体文件信息的关键工具
- PCM 数据获取需要正确配置解码器,不指定 Surface 输出
- 实时处理应该使用异步模式和后台线程
- 性能优化包括缓冲区重用、批量处理和直接缓冲区使用
通过本文的示例代码和最佳实践,开发者可以:
- ✅ 读取各种媒体文件的详细信息
- ✅ 从 MP3/MP4 中提取原始 PCM 数据
- ✅ 实时处理和分析音频数据
- ✅ 构建高效的音频处理应用
参考资料
注意:本文基于 Android API 21+,某些功能在旧版本可能不可用。在实际开发中请始终检查 API 级别兼容性,并正确处理权限和文件访问问题。
Android MediaCodec:深入理解多媒体编解码核心
https://blog.uso6.com/archives/android-mediacodec-shen-ru-li-jie-duo-mei-ti-bian-jie-ma-he-xin
评论