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...")
}
posted @ 2025-08-17 00:25  爱情丶眨眼而去  阅读(19)  评论(0)    收藏  举报