MediaCodec的使用(音频解码二)
MediaCodec的使用(音频解码二)
本节我们使用MediaCodec进行音频解码,音频解码与音频
准备工作
准备一个ogg文件,然后使用ffplay播放
ffplay audio1.ogg
PS D:\SoftWare\Android\AndroidProjects\JetpacksProjects\HelloKtorfit> ffplay .\audio1.ogg
ffplay version 7.1.1-essentials_build-www.gyan.dev Copyright (c) 2003-2025 the FFmpeg developers
built with gcc 14.2.0 (Rev1, Built by MSYS2 project)
configuration: --enable-gpl --enable-version3 --enable-static --disable-w32threads --disable-autodetect --enable-fontconfig --enable-iconv --enable-gnutls --enable-libxml2 --enable-gmp --enable-bzlib --enable-lzma --enable-zlib --enable-libsrt --enable-libssh --enable-libzmq --enable-avisynth --enable-sdl2 --enable-libwebp --ena
ble-libx264 --enable-libx265 --enable-libxvid --enable-libaom --enable-libopenjpeg --enable-libvpx --enable-mediafoundation --enable-libass --enable-libfreetype --enable-libfribidi --enable-libharfbuzz --enable-libvidstab --enable-libvmaf --enable-libzimg --enable-amf --enable-cuda-llvm --enable-cuvid --enable-dxva2 --enable-d3d11
va --enable-d3d12va --enable-ffnvcodec --enable-libvpl --enable-nvdec --enable-nvenc --enable-vaapi --enable-libgme --enable-libopenmpt --enable-libopencore-amrwb --enable-libmp3lame --enable-libtheora --enable-libvo-amrwbenc --enable-libgsm --enable-libopencore-amrnb --enable-libopus --enable-libspeex --enable-libvorbis --enable-librubberband
libavutil 59. 39.100 / 59. 39.100
libavcodec 61. 19.101 / 61. 19.101
libavformat 61. 7.100 / 61. 7.100
libavdevice 61. 3.100 / 61. 3.100
libavfilter 10. 4.100 / 10. 4.100
libswscale 8. 3.100 / 8. 3.100
libswresample 5. 3.100 / 5. 3.100
libpostproc 58. 3.100 / 58. 3.100
Input #0, ogg, from '.\audio1.ogg':0KB vq= 0KB sq= 0B
Duration: 00:06:18.10, start: 0.000000, bitrate: 347 kb/s
Stream #0:0: Audio: vorbis, 44100 Hz, stereo, fltp, 320 kb/s
Metadata:
TITLE : 春庭雪
ARTIST : 赵允哲/DJMag
end : 16419143
endserial : 1067571064
endgran : 16674166
13.96 M-A: 0.000 fd= 0 aq= 39KB vq= 0KB sq= 0B
可以看到视频详细信息。
解码
/**
* 解码 ogg 为 pcm
*/
internal suspend fun oggToPcm(context: Context, oggUri: Uri, pcmUri: Uri): Unit = suspendCancellableCoroutine { continuation ->
val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS)
// 解码器
val oggDecoders: List<MediaCodecInfo> = mediaCodecList.codecInfos.filter { !it.isEncoder }.filter {
it.supportedTypes.contains(element = MediaFormat.MIMETYPE_AUDIO_VORBIS)
}
oggDecoders.forEach { mediaCodecInfo ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.i(TAG, "oggToPcm -> name: ${mediaCodecInfo.name}, canonicalName: ${mediaCodecInfo.canonicalName}, isAlias: ${mediaCodecInfo.isAlias}, isVendor: ${mediaCodecInfo.isVendor}, isHardwareAccelerated: ${mediaCodecInfo.isHardwareAccelerated}, isEncoder: ${mediaCodecInfo.isEncoder}, isSoftwareOnly: ${mediaCodecInfo.isSoftwareOnly}, supportedTypes: ${mediaCodecInfo.supportedTypes.joinToString()}")
} else {
Log.i(TAG, "oggToPcm -> name: ${mediaCodecInfo.name}, isEncoder: ${mediaCodecInfo.isEncoder}, supportedTypes: ${mediaCodecInfo.supportedTypes.joinToString()}")
}
}
Log.i(TAG, "pcmToAac -> oggDecoders: ${oggDecoders.joinToString { it.name }}")
if (oggDecoders.isEmpty()){
// 不支持硬件解码
Log.i(TAG, "oggToPcm -> 不支持硬件解码...") // 使用软解码
if (continuation.isActive){
continuation.resume(Unit)
}
return@suspendCancellableCoroutine
}
// 可惜 aac 仅支持软解码
Log.i(TAG, "oggToPcm -> 支持硬件解码")
val mediaFormat1: MediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_RAW, 16000, 1)
val mediaExtractor: MediaExtractor = MediaExtractor()
mediaExtractor.setDataSource(context, oggUri, mapOf<String, String>())
val mediaFormat2: MediaFormat = mediaExtractor.getTrackFormat(0) // 或者提前判断trackCount > 0
Log.i(TAG, "oggToPcm -> mediaFormat1: $mediaFormat1, mediaFormat2: $mediaFormat2")
mediaExtractor.selectTrack(0)
val mediaCodec: MediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_VORBIS)
mediaCodec.configure(mediaFormat2, null, null, 0) // 解码
val pcmInputStream: InputStream = context.contentResolver.openInputStream(oggUri)!!
val aacOutputStream: OutputStream = context.contentResolver.openOutputStream(pcmUri)!!
val bytes = ByteArray(1024 * 8)
mediaCodec.setCallback(object : MediaCodec.Callback() {
override fun onError(
codec: MediaCodec,
e: MediaCodec.CodecException
) {
Log.e(
TAG,
"onError name: ${codec.name}, thread: ${Thread.currentThread()}, error: ${e.message}",
e
)
}
override fun onInputBufferAvailable(
codec: MediaCodec,
index: Int
) {
val inputBuffer: ByteBuffer = codec.getInputBuffer(index) ?: return
val size: Int = mediaExtractor.readSampleData(inputBuffer, 0)
Log.i(
TAG,
"onInputBufferAvailable -> name: ${mediaCodec.name}, index: $index, thread: ${Thread.currentThread()}, size: $size"
)
if (size > 0) {
codec.queueInputBuffer(index, 0, size, mediaExtractor.sampleTime, mediaExtractor.sampleFlags)
mediaExtractor.advance()
} else {
codec.queueInputBuffer(
index,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
}
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
Log.i(
TAG,
"onOutputBufferAvailable -> name: ${codec.name}, index: $index, infoSize: ${info.size}, thread: ${Thread.currentThread()}"
)
val outputBuffer: ByteBuffer = codec.getOutputBuffer(index) ?: return
outputBuffer.get(bytes, 0, info.size)
aacOutputStream.write(bytes, 0, info.size)
codec.releaseOutputBuffer(index, false)
if (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
Log.i(TAG, "onOutputBufferAvailable -> == 编码结束...") // todo
pcmInputStream.close()
aacOutputStream.close()
mediaExtractor.release()
if (continuation.isActive) {
Log.i(TAG, "pcmToAac -> 解码完成 resume before...")
continuation.resume(Unit)
Log.i(TAG, "pcmToAac -> 解码完成 resume after...")
}
}
}
override fun onOutputFormatChanged(
codec: MediaCodec,
format: MediaFormat
) {
Log.i(
TAG,
"onOutputFormatChanged -> name: ${codec.name}, format: $format"
)
}
})
Log.i(TAG, "pcmToAac -> before start...")
mediaCodec.start()
Log.i(TAG, "pcmToAac -> after start...")
}
细节1
看个特殊的例子
internal suspend fun oggToPcm1(context: Context, oggUri: Uri, pcmUri: Uri): Unit = suspendCancellableCoroutine { continuation ->
continuation.resume(Unit)
continuation.resume(Unit)
}
可以看到如下异常
2025-08-16 22:55:53.179 10991-10991 AndroidRuntime pid-10991 E FATAL EXCEPTION: main @coroutine#39
Process: edu.tyut.helloktorfit, PID: 10991
java.lang.IllegalStateException: Already resumed, but proposed with update kotlin.Unit
at kotlinx.coroutines.CancellableContinuationImpl.alreadyResumedError(CancellableContinuationImpl.kt:556)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$kotlinx_coroutines_core(CancellableContinuationImpl.kt:521)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$kotlinx_coroutines_core$default(CancellableContinuationImpl.kt:493)
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:359)
在回调众多的方法当中,无法确定resume是否被多次执行的可能,可以修改为如下
internal suspend fun oggToPcm1(context: Context, oggUri: Uri, pcmUri: Uri): Unit = suspendCancellableCoroutine { continuation ->
if (continuation.isActive) {
continuation.resume(Unit)
}
if (continuation.isActive) {
continuation.resume(Unit)
}
}
细节2
如果使用的是Java语言没有协程咋办?
可以使用下面的方法进行
internal fun oggToPcm1(context: Context, oggUri: Uri, pcmUri: Uri) : Unit {
Log.i(TAG, "oggToPcm1 -> launch before: ${Thread.currentThread()}")
var done = false
thread {
Thread.sleep(1000)
synchronized(this@AudioExtractManager){
Log.i(TAG, "oggToPcm1 -> notify before: ${Thread.currentThread()}")
done = true
this@AudioExtractManager.notify()
Log.i(TAG, "oggToPcm1 -> notify after: ${Thread.currentThread()}")
}
}
Log.i(TAG, "oggToPcm1 -> launch after: ${Thread.currentThread()}")
Thread.sleep(2000)
synchronized(this@AudioExtractManager){
Log.i(TAG, "oggToPcm1 -> wait before: ${Thread.currentThread()}")
while (!done) {
this@AudioExtractManager.wait()
}
Log.i(TAG, "oggToPcm1 -> wait after: ${Thread.currentThread()}")
}
Log.i(TAG, "oggToPcm1 -> finish...")
}
可以看到Java的写法显得非常low,还容易导致死锁,一定会导致线程上下文切换,消耗资源。
AAC 解码为 PCM 目前使用的软解
/**
* 解码 ogg 为 pcm
*/
internal suspend fun flacToPcm(context: Context, flacUri: Uri, pcmUri: Uri): Unit = suspendCancellableCoroutine { continuation ->
val mediaCodecList = MediaCodecList(MediaCodecList.ALL_CODECS)
// 解码器
val flacDecoders: List<MediaCodecInfo> = mediaCodecList.codecInfos.filter { !it.isEncoder }.filter {
it.supportedTypes.contains(element = MediaFormat.MIMETYPE_AUDIO_FLAC)
}
flacDecoders.forEach { mediaCodecInfo ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.i(TAG, "oggToPcm -> name: ${mediaCodecInfo.name}, canonicalName: ${mediaCodecInfo.canonicalName}, isAlias: ${mediaCodecInfo.isAlias}, isVendor: ${mediaCodecInfo.isVendor}, isHardwareAccelerated: ${mediaCodecInfo.isHardwareAccelerated}, isEncoder: ${mediaCodecInfo.isEncoder}, isSoftwareOnly: ${mediaCodecInfo.isSoftwareOnly}, supportedTypes: ${mediaCodecInfo.supportedTypes.joinToString()}")
} else {
Log.i(TAG, "oggToPcm -> name: ${mediaCodecInfo.name}, isEncoder: ${mediaCodecInfo.isEncoder}, supportedTypes: ${mediaCodecInfo.supportedTypes.joinToString()}")
}
}
Log.i(TAG, "pcmToAac -> oggDecoders: ${flacDecoders.joinToString { it.name }}")
if (flacDecoders.isEmpty()){
// 不支持硬件解码
Log.i(TAG, "oggToPcm -> 不支持硬件解码...") // 使用软解码
if (continuation.isActive){
continuation.resume(Unit)
}
return@suspendCancellableCoroutine
}
// 可惜 aac 仅支持软解码
Log.i(TAG, "oggToPcm -> 支持硬件解码")
val mediaExtractor = MediaExtractor()
mediaExtractor.setDataSource(context, flacUri, mapOf<String, String>())
val mediaFormat2: MediaFormat = mediaExtractor.getTrackFormat(0) // 或者提前判断trackCount > 0
for (i in 0 until mediaExtractor.trackCount){
Log.i(TAG, "flacToPcm -> format: ${mediaExtractor.getTrackFormat(i)}")
}
Log.i(TAG, "oggToPcm -> mediaFormat2: $mediaFormat2, trackCount: ${mediaExtractor.trackCount}")
mediaExtractor.selectTrack(0)
val mediaCodec: MediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_RAW)
mediaCodec.configure(mediaFormat2, null, null, 0) // 解码
val aacOutputStream: OutputStream = context.contentResolver.openOutputStream(pcmUri)!!
val bytes = ByteArray(1024 * 1024)
mediaCodec.setCallback(object : MediaCodec.Callback() {
override fun onError(
codec: MediaCodec,
e: MediaCodec.CodecException
) {
Log.e(
TAG,
"onError name: ${codec.name}, thread: ${Thread.currentThread()}, error: ${e.message}",
e
)
}
override fun onInputBufferAvailable(
codec: MediaCodec,
index: Int
) {
val inputBuffer: ByteBuffer = codec.getInputBuffer(index) ?: return
val size: Int = mediaExtractor.readSampleData(inputBuffer, 0)
Log.i(
TAG,
"onInputBufferAvailable -> name: ${mediaCodec.name}, index: $index, thread: ${Thread.currentThread()}, size: $size"
)
if (size > 0) {
codec.queueInputBuffer(index, 0, size, mediaExtractor.sampleTime, mediaExtractor.sampleFlags)
mediaExtractor.advance()
} else {
codec.queueInputBuffer(
index,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
}
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
Log.i(
TAG,
"onOutputBufferAvailable -> name: ${codec.name}, index: $index, infoSize: ${info.size}, thread: ${Thread.currentThread()}"
)
val outputBuffer: ByteBuffer = codec.getOutputBuffer(index) ?: return
outputBuffer.get(bytes, 0, info.size)
aacOutputStream.write(bytes, 0, info.size)
codec.releaseOutputBuffer(index, false)
if (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
Log.i(TAG, "onOutputBufferAvailable -> == 编码结束...") // todo
aacOutputStream.close()
mediaExtractor.release()
if (continuation.isActive) {
Log.i(TAG, "pcmToAac -> 解码完成 resume before...")
continuation.resume(Unit)
Log.i(TAG, "pcmToAac -> 解码完成 resume after...")
}
}
}
override fun onOutputFormatChanged(
codec: MediaCodec,
format: MediaFormat
) {
Log.i(
TAG,
"onOutputFormatChanged -> name: ${codec.name}, format: $format"
)
}
})
Log.i(TAG, "pcmToAac -> before start...")
mediaCodec.start()
Log.i(TAG, "pcmToAac -> after start...")
}

MediaCodec的使用(音频解码二)
浙公网安备 33010602011771号