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 developersbuilt 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-librubberbandlibavutil 59. 39.100 / 59. 39.100libavcodec 61. 19.101 / 61. 19.101libavformat 61. 7.100 / 61. 7.100libavdevice 61. 3.100 / 61. 3.100libavfilter 10. 4.100 / 10. 4.100libswscale 8. 3.100 / 8. 3.100libswresample 5. 3.100 / 5. 3.100libpostproc 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/sStream #0:0: Audio: vorbis, 44100 Hz, stereo, fltp, 320 kb/sMetadata:TITLE : 春庭雪ARTIST : 赵允哲/DJMagend : 16419143endserial : 1067571064endgran : 1667416613.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 > 0Log.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) ?: returnval 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) ?: returnoutputBuffer.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 -> == 编码结束...") // todopcmInputStream.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#39Process: edu.tyut.helloktorfit, PID: 10991java.lang.IllegalStateException: Already resumed, but proposed with update kotlin.Unitat 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 = falsethread {Thread.sleep(1000)synchronized(this@AudioExtractManager){Log.i(TAG, "oggToPcm1 -> notify before: ${Thread.currentThread()}")done = truethis@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 > 0for (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) ?: returnval 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) ?: returnoutputBuffer.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 -> == 编码结束...") // todoaacOutputStream.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的使用(音频解码二)